mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 22:28:25 +00:00
PKI revamp: frontend implementation
This commit is contained in:
@@ -33,7 +33,8 @@ export const getDefaultOnPremFeatures = () => {
|
|||||||
enterpriseSecretSyncs: false,
|
enterpriseSecretSyncs: false,
|
||||||
enterpriseCertificateSyncs: false,
|
enterpriseCertificateSyncs: false,
|
||||||
enterpriseAppConnections: true,
|
enterpriseAppConnections: true,
|
||||||
machineIdentityAuthTemplates: false
|
machineIdentityAuthTemplates: false,
|
||||||
|
pkiLegacyTemplates: false
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
|||||||
fips: false,
|
fips: false,
|
||||||
eventSubscriptions: false,
|
eventSubscriptions: false,
|
||||||
machineIdentityAuthTemplates: false,
|
machineIdentityAuthTemplates: false,
|
||||||
|
pkiLegacyTemplates: false,
|
||||||
pam: false
|
pam: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export type TFeatureSet = {
|
|||||||
enterpriseCertificateSyncs: false;
|
enterpriseCertificateSyncs: false;
|
||||||
enterpriseAppConnections: false;
|
enterpriseAppConnections: false;
|
||||||
machineIdentityAuthTemplates: false;
|
machineIdentityAuthTemplates: false;
|
||||||
|
pkiLegacyTemplates: false;
|
||||||
fips: false;
|
fips: false;
|
||||||
eventSubscriptions: false;
|
eventSubscriptions: false;
|
||||||
pam: false;
|
pam: false;
|
||||||
|
|||||||
@@ -69,10 +69,24 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
querystring: listCertificateProfilesSchema,
|
querystring: listCertificateProfilesSchema.extend({
|
||||||
|
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||||
|
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfiles: CertificateProfilesSchema.array(),
|
certificateProfiles: CertificateProfilesSchema.extend({
|
||||||
|
metrics: z
|
||||||
|
.object({
|
||||||
|
profileId: z.string(),
|
||||||
|
totalCertificates: z.number(),
|
||||||
|
activeCertificates: z.number(),
|
||||||
|
expiredCertificates: z.number(),
|
||||||
|
expiringCertificates: z.number(),
|
||||||
|
revokedCertificates: z.number()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}).array(),
|
||||||
totalCount: z.number()
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -112,6 +126,10 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
params: getCertificateProfileByIdSchema,
|
params: getCertificateProfileByIdSchema,
|
||||||
|
querystring: z.object({
|
||||||
|
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||||
|
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
certificateProfile: CertificateProfilesSchema.extend({
|
certificateProfile: CertificateProfilesSchema.extend({
|
||||||
@@ -145,6 +163,16 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
autoRenew: z.boolean(),
|
autoRenew: z.boolean(),
|
||||||
autoRenewDays: z.number().optional()
|
autoRenewDays: 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()
|
.optional()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -160,6 +188,20 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
profileId: req.params.id
|
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({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateProfile.projectId,
|
projectId: certificateProfile.projectId,
|
||||||
@@ -171,7 +213,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { certificateProfile };
|
return { certificateProfile: result };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -322,7 +364,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
status: z.string(),
|
status: z.string(),
|
||||||
notBefore: z.date(),
|
notBefore: z.date(),
|
||||||
notAfter: z.date(),
|
notAfter: z.date(),
|
||||||
isRevoked: z.boolean(),
|
revokedAt: z.date().nullable().optional(),
|
||||||
createdAt: z.date()
|
createdAt: z.date()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -343,45 +385,4 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
return { certificates };
|
return { certificates };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "GET",
|
|
||||||
url: "/:id/metrics",
|
|
||||||
config: {
|
|
||||||
rateLimit: readLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
hide: false,
|
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
|
||||||
params: getCertificateProfileByIdSchema,
|
|
||||||
querystring: z.object({
|
|
||||||
expiringDays: z.number().min(1).max(365).default(30)
|
|
||||||
}),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
metrics: z.object({
|
|
||||||
profileId: z.string(),
|
|
||||||
totalCertificates: z.number(),
|
|
||||||
activeCertificates: z.number(),
|
|
||||||
expiredCertificates: z.number(),
|
|
||||||
expiringCertificates: z.number(),
|
|
||||||
revokedCertificates: z.number()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
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
|
|
||||||
});
|
|
||||||
|
|
||||||
return { metrics };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,14 +33,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
altNames: validateAltNamesField.optional(),
|
altNames: validateAltNamesField.optional(),
|
||||||
organization: z.string().optional(),
|
|
||||||
organizationUnit: z.string().optional(),
|
|
||||||
locality: z.string().optional(),
|
|
||||||
state: z.string().optional(),
|
|
||||||
country: z.string().length(2).optional(),
|
|
||||||
email: z.string().email().optional(),
|
|
||||||
streetAddress: z.string().optional(),
|
|
||||||
postalCode: z.string().optional(),
|
|
||||||
signatureAlgorithm: z.string().optional(),
|
signatureAlgorithm: z.string().optional(),
|
||||||
keyAlgorithm: z.string().optional()
|
keyAlgorithm: z.string().optional()
|
||||||
}),
|
}),
|
||||||
@@ -65,14 +57,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
profileId: req.body.profileId,
|
profileId: req.body.profileId,
|
||||||
certificateRequest: {
|
certificateRequest: {
|
||||||
commonName: req.body.commonName,
|
commonName: req.body.commonName,
|
||||||
organization: req.body.organization,
|
|
||||||
organizationUnit: req.body.organizationUnit,
|
|
||||||
locality: req.body.locality,
|
|
||||||
state: req.body.state,
|
|
||||||
country: req.body.country,
|
|
||||||
email: req.body.email,
|
|
||||||
streetAddress: req.body.streetAddress,
|
|
||||||
postalCode: req.body.postalCode,
|
|
||||||
keyUsages: req.body.keyUsages,
|
keyUsages: req.body.keyUsages,
|
||||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
subjectAlternativeNames: req.body.altNames
|
subjectAlternativeNames: req.body.altNames
|
||||||
@@ -221,14 +205,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
notBefore: validateCaDateField.optional(),
|
notBefore: validateCaDateField.optional(),
|
||||||
notAfter: validateCaDateField.optional(),
|
notAfter: validateCaDateField.optional(),
|
||||||
commonName: validateTemplateRegexField.optional(),
|
commonName: validateTemplateRegexField.optional(),
|
||||||
organization: z.string().optional(),
|
|
||||||
organizationUnit: z.string().optional(),
|
|
||||||
locality: z.string().optional(),
|
|
||||||
state: z.string().optional(),
|
|
||||||
country: z.string().length(2).optional(),
|
|
||||||
email: z.string().email().optional(),
|
|
||||||
streetAddress: z.string().optional(),
|
|
||||||
postalCode: z.string().optional(),
|
|
||||||
signatureAlgorithm: z.string().optional(),
|
signatureAlgorithm: z.string().optional(),
|
||||||
keyAlgorithm: z.string().optional()
|
keyAlgorithm: z.string().optional()
|
||||||
}),
|
}),
|
||||||
@@ -280,14 +256,6 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
commonName: req.body.commonName,
|
commonName: req.body.commonName,
|
||||||
organization: req.body.organization,
|
|
||||||
organizationUnit: req.body.organizationUnit,
|
|
||||||
locality: req.body.locality,
|
|
||||||
state: req.body.state,
|
|
||||||
country: req.body.country,
|
|
||||||
email: req.body.email,
|
|
||||||
streetAddress: req.body.streetAddress,
|
|
||||||
postalCode: req.body.postalCode,
|
|
||||||
keyUsages: req.body.keyUsages,
|
keyUsages: req.body.keyUsages,
|
||||||
extendedKeyUsages: req.body.extendedKeyUsages,
|
extendedKeyUsages: req.body.extendedKeyUsages,
|
||||||
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
notBefore: req.body.notBefore ? new Date(req.body.notBefore) : undefined,
|
||||||
|
|||||||
@@ -127,11 +127,21 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
|
includeMetrics?: boolean;
|
||||||
|
expiringDays?: number;
|
||||||
} = {},
|
} = {},
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const { offset = 0, limit = 20, search, enrollmentType, caId } = options;
|
const {
|
||||||
|
offset = 0,
|
||||||
|
limit = 20,
|
||||||
|
search,
|
||||||
|
enrollmentType,
|
||||||
|
caId,
|
||||||
|
includeMetrics = false,
|
||||||
|
expiringDays = 7
|
||||||
|
} = options;
|
||||||
|
|
||||||
let query = (tx || db)(TableName.CertificateProfile).where(
|
let query = (tx || db)(TableName.CertificateProfile).where(
|
||||||
`${TableName.CertificateProfile}.projectId`,
|
`${TableName.CertificateProfile}.projectId`,
|
||||||
@@ -155,6 +165,38 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
query = query.where(`${TableName.CertificateProfile}.caId`, caId);
|
query = query.where(`${TableName.CertificateProfile}.caId`, caId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeMetrics) {
|
||||||
|
const now = new Date();
|
||||||
|
const expiringDate = new Date();
|
||||||
|
expiringDate.setDate(now.getDate() + expiringDays);
|
||||||
|
|
||||||
|
const certificateProfiles = await query
|
||||||
|
.leftJoin(TableName.Certificate, `${TableName.CertificateProfile}.id`, `${TableName.Certificate}.profileId`)
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.CertificateProfile),
|
||||||
|
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.CertificateProfile}.id`)
|
||||||
|
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc")
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
return certificateProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
const certificateProfiles = await query
|
const certificateProfiles = await query
|
||||||
.select(selectAllTableCols(TableName.CertificateProfile))
|
.select(selectAllTableCols(TableName.CertificateProfile))
|
||||||
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc")
|
.orderBy(`${TableName.CertificateProfile}.createdAt`, "desc")
|
||||||
@@ -239,13 +281,13 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
if (status) {
|
if (status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "active":
|
case "active":
|
||||||
query = query.where("notAfter", ">", now).where("isRevoked", false);
|
query = query.where("notAfter", ">", now).whereNull("revokedAt");
|
||||||
break;
|
break;
|
||||||
case "expired":
|
case "expired":
|
||||||
query = query.where("notAfter", "<=", now).where("isRevoked", false);
|
query = query.where("notAfter", "<=", now).whereNull("revokedAt");
|
||||||
break;
|
break;
|
||||||
case "revoked":
|
case "revoked":
|
||||||
query = query.where("isRevoked", true);
|
query = query.whereNotNull("revokedAt");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -259,7 +301,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
.select((tx || db).ref("status").withSchema(TableName.Certificate))
|
.select((tx || db).ref("status").withSchema(TableName.Certificate))
|
||||||
.select((tx || db).ref("notBefore").withSchema(TableName.Certificate))
|
.select((tx || db).ref("notBefore").withSchema(TableName.Certificate))
|
||||||
.select((tx || db).ref("notAfter").withSchema(TableName.Certificate))
|
.select((tx || db).ref("notAfter").withSchema(TableName.Certificate))
|
||||||
.select((tx || db).ref("isRevoked").withSchema(TableName.Certificate))
|
.select((tx || db).ref("revokedAt").withSchema(TableName.Certificate))
|
||||||
.select((tx || db).ref("createdAt").withSchema(TableName.Certificate))
|
.select((tx || db).ref("createdAt").withSchema(TableName.Certificate))
|
||||||
.orderBy("createdAt", "desc")
|
.orderBy("createdAt", "desc")
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
@@ -273,7 +315,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const getProfileMetrics = async (
|
const getProfileMetrics = async (
|
||||||
profileId: string,
|
profileId: string,
|
||||||
expiringDays: number = 30,
|
expiringDays: number = 7,
|
||||||
tx?: Knex
|
tx?: Knex
|
||||||
): Promise<TCertificateProfileMetrics> => {
|
): Promise<TCertificateProfileMetrics> => {
|
||||||
try {
|
try {
|
||||||
@@ -285,13 +327,15 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
.where("profileId", profileId)
|
.where("profileId", profileId)
|
||||||
.select(
|
.select(
|
||||||
db.raw("COUNT(*) as total_certificates"),
|
db.raw("COUNT(*) as total_certificates"),
|
||||||
db.raw("COUNT(CASE WHEN NOT is_revoked AND not_after > ? THEN 1 END) as active_certificates", [now]),
|
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? THEN 1 END) as active_certificates', [
|
||||||
db.raw("COUNT(CASE WHEN NOT is_revoked AND not_after <= ? THEN 1 END) as expired_certificates", [now]),
|
expiringDate
|
||||||
|
]),
|
||||||
|
db.raw('COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" <= ? THEN 1 END) as expired_certificates', [now]),
|
||||||
db.raw(
|
db.raw(
|
||||||
"COUNT(CASE WHEN NOT is_revoked AND not_after > ? AND not_after <= ? THEN 1 END) as expiring_certificates",
|
'COUNT(CASE WHEN "revokedAt" IS NULL AND "notAfter" > ? AND "notAfter" <= ? THEN 1 END) as expiring_certificates',
|
||||||
[now, expiringDate]
|
[now, expiringDate]
|
||||||
),
|
),
|
||||||
db.raw("COUNT(CASE WHEN is_revoked THEN 1 END) as revoked_certificates")
|
db.raw('COUNT(CASE WHEN "revokedAt" IS NOT NULL THEN 1 END) as revoked_certificates')
|
||||||
)
|
)
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
|
|||||||
@@ -466,7 +466,9 @@ describe("CertificateProfileService", () => {
|
|||||||
limit: 20,
|
limit: 20,
|
||||||
search: undefined,
|
search: undefined,
|
||||||
enrollmentType: undefined,
|
enrollmentType: undefined,
|
||||||
caId: undefined
|
caId: undefined,
|
||||||
|
includeMetrics: false,
|
||||||
|
expiringDays: 30
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -486,7 +488,51 @@ describe("CertificateProfileService", () => {
|
|||||||
limit: 5,
|
limit: 5,
|
||||||
search: "test",
|
search: "test",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123"
|
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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import {
|
|||||||
TCertificateProfileInsert,
|
TCertificateProfileInsert,
|
||||||
TCertificateProfileMetrics,
|
TCertificateProfileMetrics,
|
||||||
TCertificateProfileUpdate,
|
TCertificateProfileUpdate,
|
||||||
TCertificateProfileWithConfigs
|
TCertificateProfileWithConfigs,
|
||||||
|
TCertificateProfileWithRawMetrics
|
||||||
} from "./certificate-profile-types";
|
} from "./certificate-profile-types";
|
||||||
|
|
||||||
export type TCertificateProfileCreateData = Omit<TCertificateProfileInsert, "estConfigId" | "apiConfigId"> & {
|
export type TCertificateProfileCreateData = Omit<TCertificateProfileInsert, "estConfigId" | "apiConfigId"> & {
|
||||||
@@ -48,10 +49,6 @@ const convertDalToService = (dalResult: Record<string, unknown>): TCertificatePr
|
|||||||
} as TCertificateProfile;
|
} as TCertificateProfile;
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertDalArrayToService = (dalResults: Record<string, unknown>[]): TCertificateProfile[] => {
|
|
||||||
return dalResults.map(convertDalToService);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateEnrollmentConfig = async (data: {
|
const validateEnrollmentConfig = async (data: {
|
||||||
enrollmentType: EnrollmentType;
|
enrollmentType: EnrollmentType;
|
||||||
estConfig?: TEstConfigData | null;
|
estConfig?: TEstConfigData | null;
|
||||||
@@ -289,14 +286,18 @@ export const certificateProfileServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
profileId
|
profileId,
|
||||||
|
includeMetrics = false,
|
||||||
|
expiringDays = 30
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
actorAuthMethod: ActorAuthMethod;
|
actorAuthMethod: ActorAuthMethod;
|
||||||
actorOrgId: string;
|
actorOrgId: string;
|
||||||
profileId: string;
|
profileId: string;
|
||||||
}): Promise<TCertificateProfile> => {
|
includeMetrics?: boolean;
|
||||||
|
expiringDays?: number;
|
||||||
|
}): Promise<TCertificateProfile & { metrics?: TCertificateProfileMetrics }> => {
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
const profile = await certificateProfileDAL.findById(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
throw new NotFoundError({ message: "Certificate profile not found" });
|
throw new NotFoundError({ message: "Certificate profile not found" });
|
||||||
@@ -315,7 +316,17 @@ export const certificateProfileServiceFactory = ({
|
|||||||
ProjectPermissionSub.CertificateProfiles
|
ProjectPermissionSub.CertificateProfiles
|
||||||
);
|
);
|
||||||
|
|
||||||
return convertDalToService(profile);
|
const converted = convertDalToService(profile);
|
||||||
|
|
||||||
|
if (includeMetrics) {
|
||||||
|
const metrics = await certificateProfileDAL.getProfileMetrics(profileId, expiringDays);
|
||||||
|
return {
|
||||||
|
...converted,
|
||||||
|
metrics
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return converted;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProfileByIdWithConfigs = async ({
|
const getProfileByIdWithConfigs = async ({
|
||||||
@@ -401,7 +412,9 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit = 20,
|
limit = 20,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
caId
|
caId,
|
||||||
|
includeMetrics = false,
|
||||||
|
expiringDays = 30
|
||||||
}: {
|
}: {
|
||||||
actor: ActorType;
|
actor: ActorType;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
@@ -413,8 +426,10 @@ export const certificateProfileServiceFactory = ({
|
|||||||
search?: string;
|
search?: string;
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
caId?: string;
|
caId?: string;
|
||||||
|
includeMetrics?: boolean;
|
||||||
|
expiringDays?: number;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
profiles: TCertificateProfile[];
|
profiles: (TCertificateProfile & { metrics?: TCertificateProfileMetrics })[];
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
}> => {
|
}> => {
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
@@ -435,7 +450,9 @@ export const certificateProfileServiceFactory = ({
|
|||||||
limit,
|
limit,
|
||||||
search,
|
search,
|
||||||
enrollmentType,
|
enrollmentType,
|
||||||
caId
|
caId,
|
||||||
|
includeMetrics,
|
||||||
|
expiringDays
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
const totalCount = await certificateProfileDAL.countByProjectId(projectId, {
|
||||||
@@ -444,8 +461,27 @@ export const certificateProfileServiceFactory = ({
|
|||||||
caId
|
caId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const convertedProfiles = profiles.map((profile) => {
|
||||||
|
const converted = convertDalToService(profile);
|
||||||
|
if (includeMetrics) {
|
||||||
|
const profileWithMetrics = profile as TCertificateProfileWithRawMetrics;
|
||||||
|
return {
|
||||||
|
...converted,
|
||||||
|
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 converted;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profiles: convertDalArrayToService(profiles),
|
profiles: convertedProfiles,
|
||||||
totalCount
|
totalCount
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
|||||||
autoRenew: boolean;
|
autoRenew: boolean;
|
||||||
autoRenewDays?: number;
|
autoRenewDays?: number;
|
||||||
};
|
};
|
||||||
|
metrics?: TCertificateProfileMetrics;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TCertificateProfileMetrics {
|
export interface TCertificateProfileMetrics {
|
||||||
@@ -63,6 +64,14 @@ export interface TCertificateProfileCertificate {
|
|||||||
status: string;
|
status: string;
|
||||||
notBefore: Date;
|
notBefore: Date;
|
||||||
notAfter: Date;
|
notAfter: Date;
|
||||||
isRevoked: boolean;
|
revokedAt: Date | null | undefined;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TCertificateProfileWithRawMetrics = TCertificateProfile & {
|
||||||
|
total_certificates?: string;
|
||||||
|
active_certificates?: string;
|
||||||
|
expired_certificates?: string;
|
||||||
|
expiring_certificates?: string;
|
||||||
|
revoked_certificates?: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const attributeTypeSchema = z.enum([
|
const attributeTypeSchema = z.enum(["common_name"]);
|
||||||
"common_name",
|
|
||||||
"organization_name",
|
|
||||||
"organization_unit",
|
|
||||||
"locality",
|
|
||||||
"state",
|
|
||||||
"country",
|
|
||||||
"email",
|
|
||||||
"street_address",
|
|
||||||
"postal_code"
|
|
||||||
]);
|
|
||||||
|
|
||||||
const includeTypeSchema = z.enum(["mandatory", "optional", "prohibit"]);
|
const includeTypeSchema = z.enum(["mandatory", "optional", "prohibit"]);
|
||||||
|
|
||||||
@@ -137,14 +127,6 @@ export const deleteCertificateTemplateV2Schema = z.object({
|
|||||||
|
|
||||||
export const certificateRequestSchema = z.object({
|
export const certificateRequestSchema = z.object({
|
||||||
commonName: z.string().optional(),
|
commonName: z.string().optional(),
|
||||||
organization: z.string().optional(),
|
|
||||||
organizationUnit: z.string().optional(),
|
|
||||||
locality: z.string().optional(),
|
|
||||||
state: z.string().optional(),
|
|
||||||
country: z.string().length(2).optional(),
|
|
||||||
email: z.string().email().optional(),
|
|
||||||
streetAddress: z.string().optional(),
|
|
||||||
postalCode: z.string().optional(),
|
|
||||||
keyUsages: z.array(z.string()).optional(),
|
keyUsages: z.array(z.string()).optional(),
|
||||||
extendedKeyUsages: z.array(z.string()).optional(),
|
extendedKeyUsages: z.array(z.string()).optional(),
|
||||||
subjectAlternativeNames: z
|
subjectAlternativeNames: z
|
||||||
|
|||||||
+4
-61
@@ -60,14 +60,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
type: "common_name",
|
type: "common_name",
|
||||||
include: "mandatory",
|
include: "mandatory",
|
||||||
value: ["example.com"]
|
value: ["example.com"]
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "organization_name",
|
|
||||||
include: "optional"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "country",
|
|
||||||
include: "prohibit"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
keyUsages: {
|
keyUsages: {
|
||||||
@@ -416,7 +408,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
describe("validateCertificateRequest", () => {
|
describe("validateCertificateRequest", () => {
|
||||||
const validRequest: TCertificateRequest = {
|
const validRequest: TCertificateRequest = {
|
||||||
commonName: "example.com",
|
commonName: "example.com",
|
||||||
organization: "Example Corp",
|
|
||||||
keyUsages: ["digital_signature", "key_encipherment"],
|
keyUsages: ["digital_signature", "key_encipherment"],
|
||||||
extendedKeyUsages: ["server_auth"],
|
extendedKeyUsages: ["server_auth"],
|
||||||
subjectAlternativeNames: [
|
subjectAlternativeNames: [
|
||||||
@@ -458,15 +449,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
expect(result.errors).toContain("common_name is mandatory but not provided in request");
|
expect(result.errors).toContain("common_name is mandatory but not provided in request");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect prohibited attributes", async () => {
|
|
||||||
const invalidRequest = { ...validRequest, country: "US" };
|
|
||||||
|
|
||||||
const result = await service.validateCertificateRequest("template-123", invalidRequest);
|
|
||||||
|
|
||||||
expect(result.isValid).toBe(false);
|
|
||||||
expect(result.errors).toContain("country is prohibited by template policy");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should validate attribute values against allowed list", async () => {
|
it("should validate attribute values against allowed list", async () => {
|
||||||
const invalidRequest = { ...validRequest, commonName: "forbidden.com" };
|
const invalidRequest = { ...validRequest, commonName: "forbidden.com" };
|
||||||
|
|
||||||
@@ -614,14 +596,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow optional attributes when not provided", async () => {
|
|
||||||
const requestWithoutOrg = { ...validRequest, organization: undefined };
|
|
||||||
|
|
||||||
const result = await service.validateCertificateRequest("template-123", requestWithoutOrg);
|
|
||||||
|
|
||||||
expect(result.isValid).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should allow optional key usages and extended key usages", async () => {
|
it("should allow optional key usages and extended key usages", async () => {
|
||||||
const requestWithOptionalUsages = {
|
const requestWithOptionalUsages = {
|
||||||
...validRequest,
|
...validRequest,
|
||||||
@@ -805,21 +779,14 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
expect(result.isValid).toBe(true);
|
expect(result.isValid).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle template with all fields prohibited", async () => {
|
it("should handle template with SAN fields prohibited", async () => {
|
||||||
const prohibitTemplate = {
|
const prohibitTemplate = {
|
||||||
...sampleTemplate,
|
...sampleTemplate,
|
||||||
attributes: [
|
attributes: [
|
||||||
{
|
{
|
||||||
type: "organization_name",
|
type: "common_name",
|
||||||
include: "prohibit" as const
|
include: "mandatory" as const,
|
||||||
},
|
value: ["example.com"]
|
||||||
{
|
|
||||||
type: "locality",
|
|
||||||
include: "prohibit" as const
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "country",
|
|
||||||
include: "prohibit" as const
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
keyUsages: {
|
keyUsages: {
|
||||||
@@ -845,9 +812,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
|
|
||||||
const requestWithProhibited = {
|
const requestWithProhibited = {
|
||||||
commonName: "example.com",
|
commonName: "example.com",
|
||||||
organization: "Test Org",
|
|
||||||
locality: "Test City",
|
|
||||||
country: "US",
|
|
||||||
keyUsages: ["digital_signature"],
|
keyUsages: ["digital_signature"],
|
||||||
extendedKeyUsages: ["server_auth"],
|
extendedKeyUsages: ["server_auth"],
|
||||||
subjectAlternativeNames: [
|
subjectAlternativeNames: [
|
||||||
@@ -859,9 +823,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
|
|
||||||
const result = await service.validateCertificateRequest("template-123", requestWithProhibited);
|
const result = await service.validateCertificateRequest("template-123", requestWithProhibited);
|
||||||
expect(result.isValid).toBe(false);
|
expect(result.isValid).toBe(false);
|
||||||
expect(result.errors).toContain("organization_name is prohibited by template policy");
|
|
||||||
expect(result.errors).toContain("locality is prohibited by template policy");
|
|
||||||
expect(result.errors).toContain("country is prohibited by template policy");
|
|
||||||
expect(result.errors).toContain("email SAN is prohibited by template policy");
|
expect(result.errors).toContain("email SAN is prohibited by template policy");
|
||||||
expect(result.errors).toContain("uri SAN is prohibited by template policy");
|
expect(result.errors).toContain("uri SAN is prohibited by template policy");
|
||||||
});
|
});
|
||||||
@@ -874,16 +835,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
type: "common_name",
|
type: "common_name",
|
||||||
include: "mandatory" as const,
|
include: "mandatory" as const,
|
||||||
value: ["example.com", "test.com"]
|
value: ["example.com", "test.com"]
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "organization_name",
|
|
||||||
include: "optional" as const,
|
|
||||||
value: ["Example Corp", "Test Corp"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "country",
|
|
||||||
include: "mandatory" as const,
|
|
||||||
value: ["US", "CA"]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
subjectAlternativeNames: [
|
subjectAlternativeNames: [
|
||||||
@@ -898,8 +849,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
|
|
||||||
const validConstrainedRequest = {
|
const validConstrainedRequest = {
|
||||||
commonName: "example.com",
|
commonName: "example.com",
|
||||||
organization: "Example Corp",
|
|
||||||
country: "US",
|
|
||||||
keyUsages: ["digital_signature", "key_encipherment"],
|
keyUsages: ["digital_signature", "key_encipherment"],
|
||||||
extendedKeyUsages: ["server_auth"],
|
extendedKeyUsages: ["server_auth"],
|
||||||
validity: { ttl: "30d" }
|
validity: { ttl: "30d" }
|
||||||
@@ -910,8 +859,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
|
|
||||||
const invalidConstrainedRequest = {
|
const invalidConstrainedRequest = {
|
||||||
commonName: "forbidden.com",
|
commonName: "forbidden.com",
|
||||||
organization: "Forbidden Corp",
|
|
||||||
country: "FR",
|
|
||||||
keyUsages: ["digital_signature", "key_encipherment"],
|
keyUsages: ["digital_signature", "key_encipherment"],
|
||||||
extendedKeyUsages: ["server_auth"],
|
extendedKeyUsages: ["server_auth"],
|
||||||
validity: { ttl: "30d" }
|
validity: { ttl: "30d" }
|
||||||
@@ -920,10 +867,6 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
const invalidResult = await service.validateCertificateRequest("template-123", invalidConstrainedRequest);
|
const invalidResult = await service.validateCertificateRequest("template-123", invalidConstrainedRequest);
|
||||||
expect(invalidResult.isValid).toBe(false);
|
expect(invalidResult.isValid).toBe(false);
|
||||||
expect(invalidResult.errors).toContain("common_name value 'forbidden.com' is not in allowed values list");
|
expect(invalidResult.errors).toContain("common_name value 'forbidden.com' is not in allowed values list");
|
||||||
expect(invalidResult.errors).toContain(
|
|
||||||
"organization_name value 'Forbidden Corp' does not match allowed patterns: Example Corp, Test Corp"
|
|
||||||
);
|
|
||||||
expect(invalidResult.errors).toContain("country value 'FR' is not in allowed values list");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should validate SAN value constraints with multiple types", async () => {
|
it("should validate SAN value constraints with multiple types", async () => {
|
||||||
|
|||||||
@@ -72,22 +72,6 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
switch (attrType) {
|
switch (attrType) {
|
||||||
case "common_name":
|
case "common_name":
|
||||||
return request.commonName;
|
return request.commonName;
|
||||||
case "organization_name":
|
|
||||||
return request.organization;
|
|
||||||
case "organization_unit":
|
|
||||||
return request.organizationUnit;
|
|
||||||
case "locality":
|
|
||||||
return request.locality;
|
|
||||||
case "state":
|
|
||||||
return request.state;
|
|
||||||
case "country":
|
|
||||||
return request.country;
|
|
||||||
case "email":
|
|
||||||
return request.email;
|
|
||||||
case "street_address":
|
|
||||||
return request.streetAddress;
|
|
||||||
case "postal_code":
|
|
||||||
return request.postalCode;
|
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -145,8 +129,25 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
if (!requestValue) {
|
if (!requestValue) {
|
||||||
errors.push(`${attrPolicy.type} is mandatory but not provided in request`);
|
errors.push(`${attrPolicy.type} is mandatory but not provided in request`);
|
||||||
} else if (attrPolicy.value && attrPolicy.value.length > 0) {
|
} else if (attrPolicy.value && attrPolicy.value.length > 0) {
|
||||||
if (!attrPolicy.value.includes(requestValue)) {
|
// Check if the request value matches any allowed pattern
|
||||||
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
|
||||||
|
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
||||||
|
if (allowedValue.includes("*")) {
|
||||||
|
// Handle wildcard patterns
|
||||||
|
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||||
|
const regex = new RE2(`^${pattern}$`);
|
||||||
|
return regex.test(requestValue);
|
||||||
|
}
|
||||||
|
return allowedValue === requestValue;
|
||||||
|
});
|
||||||
|
if (!isValidValue) {
|
||||||
|
if (hasWildcards) {
|
||||||
|
errors.push(
|
||||||
|
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,18 +157,24 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attrPolicy.include === "optional" && requestValue && attrPolicy.value && attrPolicy.value.length > 0) {
|
if (attrPolicy.include === "optional" && requestValue && attrPolicy.value && attrPolicy.value.length > 0) {
|
||||||
|
const hasWildcards = attrPolicy.value.some((val) => val.includes("*"));
|
||||||
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
const isValidValue = attrPolicy.value.some((allowedValue) => {
|
||||||
if (allowedValue.includes("*")) {
|
if (allowedValue.includes("*")) {
|
||||||
const pattern = allowedValue.replace(/\*/g, "[^.]*");
|
// Handle wildcard patterns - escape dots and replace * with .*
|
||||||
|
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||||
const regex = new RE2(`^${pattern}$`);
|
const regex = new RE2(`^${pattern}$`);
|
||||||
return regex.test(requestValue);
|
return regex.test(requestValue);
|
||||||
}
|
}
|
||||||
return allowedValue === requestValue;
|
return allowedValue === requestValue;
|
||||||
});
|
});
|
||||||
if (!isValidValue) {
|
if (!isValidValue) {
|
||||||
errors.push(
|
if (hasWildcards) {
|
||||||
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
errors.push(
|
||||||
);
|
`${attrPolicy.type} value '${requestValue}' does not match allowed patterns: ${attrPolicy.value.join(", ")}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errors.push(`${attrPolicy.type} value '${requestValue}' is not in allowed values list`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -216,9 +223,25 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
if (requestSans.length === 0) {
|
if (requestSans.length === 0) {
|
||||||
errors.push(`${sanPolicy.type} SAN is mandatory but not provided in request`);
|
errors.push(`${sanPolicy.type} SAN is mandatory but not provided in request`);
|
||||||
} else if (sanPolicy.value && sanPolicy.value.length > 0) {
|
} else if (sanPolicy.value && sanPolicy.value.length > 0) {
|
||||||
|
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
|
||||||
requestSans.forEach((san) => {
|
requestSans.forEach((san) => {
|
||||||
if (!sanPolicy.value!.includes(san.value)) {
|
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
||||||
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
if (allowedValue.includes("*")) {
|
||||||
|
// Handle wildcard patterns - escape dots and replace * with .*
|
||||||
|
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||||
|
const regex = new RE2(`^${pattern}$`);
|
||||||
|
return regex.test(san.value);
|
||||||
|
}
|
||||||
|
return allowedValue === san.value;
|
||||||
|
});
|
||||||
|
if (!isValidValue) {
|
||||||
|
if (hasWildcards) {
|
||||||
|
errors.push(
|
||||||
|
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -229,19 +252,25 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sanPolicy.include === "optional" && sanPolicy.value && sanPolicy.value.length > 0) {
|
if (sanPolicy.include === "optional" && sanPolicy.value && sanPolicy.value.length > 0) {
|
||||||
|
const hasWildcards = sanPolicy.value.some((val) => val.includes("*"));
|
||||||
requestSans.forEach((san) => {
|
requestSans.forEach((san) => {
|
||||||
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
const isValidValue = sanPolicy.value!.some((allowedValue) => {
|
||||||
if (allowedValue.includes("*")) {
|
if (allowedValue.includes("*")) {
|
||||||
const pattern = allowedValue.replace(/\*/g, "[^.]*");
|
// Handle wildcard patterns - escape dots and replace * with .*
|
||||||
|
const pattern = allowedValue.replace(/\./g, "\\.").replace(/\*/g, ".*");
|
||||||
const regex = new RE2(`^${pattern}$`);
|
const regex = new RE2(`^${pattern}$`);
|
||||||
return regex.test(san.value);
|
return regex.test(san.value);
|
||||||
}
|
}
|
||||||
return allowedValue === san.value;
|
return allowedValue === san.value;
|
||||||
});
|
});
|
||||||
if (!isValidValue) {
|
if (!isValidValue) {
|
||||||
errors.push(
|
if (hasWildcards) {
|
||||||
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
errors.push(
|
||||||
);
|
`${sanPolicy.type} SAN value '${san.value}' does not match allowed patterns: ${sanPolicy.value!.join(", ")}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
errors.push(`${sanPolicy.type} SAN value '${san.value}' is not in allowed values list`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,7 @@ import { TCertificateTemplatesV2, TCertificateTemplatesV2Insert } from "@app/db/
|
|||||||
|
|
||||||
export interface TTemplateV2Policy {
|
export interface TTemplateV2Policy {
|
||||||
attributes: Array<{
|
attributes: Array<{
|
||||||
type:
|
type: "common_name";
|
||||||
| "common_name"
|
|
||||||
| "organization_name"
|
|
||||||
| "organization_unit"
|
|
||||||
| "locality"
|
|
||||||
| "state"
|
|
||||||
| "country"
|
|
||||||
| "email"
|
|
||||||
| "street_address"
|
|
||||||
| "postal_code";
|
|
||||||
include: "mandatory" | "optional" | "prohibit";
|
include: "mandatory" | "optional" | "prohibit";
|
||||||
value?: string[];
|
value?: string[];
|
||||||
}>;
|
}>;
|
||||||
@@ -97,14 +88,6 @@ export type TCertificateTemplateV2Update = Partial<
|
|||||||
|
|
||||||
export interface TCertificateRequest {
|
export interface TCertificateRequest {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
organization?: string;
|
|
||||||
organizationUnit?: string;
|
|
||||||
locality?: string;
|
|
||||||
state?: string;
|
|
||||||
country?: string;
|
|
||||||
email?: string;
|
|
||||||
streetAddress?: string;
|
|
||||||
postalCode?: string;
|
|
||||||
keyUsages?: string[];
|
keyUsages?: string[];
|
||||||
extendedKeyUsages?: string[];
|
extendedKeyUsages?: string[];
|
||||||
subjectAlternativeNames?: Array<{
|
subjectAlternativeNames?: Array<{
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ describe("CertificateV3Service", () => {
|
|||||||
describe("issueCertificateFromProfile", () => {
|
describe("issueCertificateFromProfile", () => {
|
||||||
const mockCertificateRequest = {
|
const mockCertificateRequest = {
|
||||||
commonName: "test.example.com",
|
commonName: "test.example.com",
|
||||||
organization: "Test Org",
|
|
||||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
||||||
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
|
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH],
|
||||||
validity: { ttl: "30d" },
|
validity: { ttl: "30d" },
|
||||||
|
|||||||
@@ -291,14 +291,6 @@ export const certificateV3ServiceFactory = ({
|
|||||||
|
|
||||||
const certificateRequest = {
|
const certificateRequest = {
|
||||||
commonName: certificateOrder.commonName,
|
commonName: certificateOrder.commonName,
|
||||||
organization: certificateOrder.organization,
|
|
||||||
organizationUnit: certificateOrder.organizationUnit,
|
|
||||||
locality: certificateOrder.locality,
|
|
||||||
state: certificateOrder.state,
|
|
||||||
country: certificateOrder.country,
|
|
||||||
email: certificateOrder.email,
|
|
||||||
streetAddress: certificateOrder.streetAddress,
|
|
||||||
postalCode: certificateOrder.postalCode,
|
|
||||||
keyUsages: certificateOrder.keyUsages,
|
keyUsages: certificateOrder.keyUsages,
|
||||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||||
subjectAlternativeNames: certificateOrder.identifiers.map((id) => ({
|
subjectAlternativeNames: certificateOrder.identifiers.map((id) => ({
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ export type TIssueCertificateFromProfileDTO = {
|
|||||||
profileId: string;
|
profileId: string;
|
||||||
certificateRequest: {
|
certificateRequest: {
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
organization?: string;
|
|
||||||
organizationUnit?: string;
|
|
||||||
locality?: string;
|
|
||||||
state?: string;
|
|
||||||
country?: string;
|
|
||||||
email?: string;
|
|
||||||
streetAddress?: string;
|
|
||||||
postalCode?: string;
|
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsage[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
subjectAlternativeNames?: Array<{
|
subjectAlternativeNames?: Array<{
|
||||||
@@ -51,14 +43,6 @@ export type TOrderCertificateFromProfileDTO = {
|
|||||||
ttl: string;
|
ttl: string;
|
||||||
};
|
};
|
||||||
commonName?: string;
|
commonName?: string;
|
||||||
organization?: string;
|
|
||||||
organizationUnit?: string;
|
|
||||||
locality?: string;
|
|
||||||
state?: string;
|
|
||||||
country?: string;
|
|
||||||
email?: string;
|
|
||||||
streetAddress?: string;
|
|
||||||
postalCode?: string;
|
|
||||||
keyUsages?: CertKeyUsage[];
|
keyUsages?: CertKeyUsage[];
|
||||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
notBefore?: Date;
|
notBefore?: Date;
|
||||||
|
|||||||
+4
-1
@@ -199,7 +199,10 @@ export const SecretSyncReviewFields = () => {
|
|||||||
</p>
|
</p>
|
||||||
{duplicateProjectId && (
|
{duplicateProjectId && (
|
||||||
<p className="mt-1 text-xs text-yellow-200">
|
<p className="mt-1 text-xs text-yellow-200">
|
||||||
Duplicate found in project ID: <code className="rounded bg-yellow-800/50 px-1 py-0.5">{duplicateProjectId}</code>
|
Duplicate found in project ID:{" "}
|
||||||
|
<code className="rounded bg-yellow-800/50 px-1 py-0.5">
|
||||||
|
{duplicateProjectId}
|
||||||
|
</code>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { AcmeDnsProvider, CaRenewalType, CaStatus, CaType, InternalCaType } from
|
|||||||
export {
|
export {
|
||||||
useCreateCa,
|
useCreateCa,
|
||||||
useCreateCertificate,
|
useCreateCertificate,
|
||||||
|
useCreateCertificateV3,
|
||||||
useDeleteCa,
|
useDeleteCa,
|
||||||
useImportCaCertificate,
|
useImportCaCertificate,
|
||||||
useRenewCa,
|
useRenewCa,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
TCreateCertificateAuthorityDTO,
|
TCreateCertificateAuthorityDTO,
|
||||||
TCreateCertificateDTO,
|
TCreateCertificateDTO,
|
||||||
TCreateCertificateResponse,
|
TCreateCertificateResponse,
|
||||||
|
TCreateCertificateV3DTO,
|
||||||
|
TCreateCertificateV3Response,
|
||||||
TDeleteCertificateAuthorityDTO,
|
TDeleteCertificateAuthorityDTO,
|
||||||
TImportCaCertificateDTO,
|
TImportCaCertificateDTO,
|
||||||
TImportCaCertificateResponse,
|
TImportCaCertificateResponse,
|
||||||
@@ -148,6 +150,24 @@ export const useCreateCertificate = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateCertificateV3 = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TCreateCertificateV3Response, object, TCreateCertificateV3DTO>({
|
||||||
|
mutationFn: async (body) => {
|
||||||
|
const { data } = await apiRequest.post<TCreateCertificateV3Response>(
|
||||||
|
"/api/v3/certificates/issue-certificate",
|
||||||
|
body
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectSlug }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectKeys.forProjectCertificates(projectSlug)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useRenewCa = () => {
|
export const useRenewCa = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TRenewCaResponse, object, TRenewCaDTO>({
|
return useMutation<TRenewCaResponse, object, TRenewCaDTO>({
|
||||||
|
|||||||
@@ -171,6 +171,32 @@ export type TCreateCertificateResponse = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreateCertificateV3DTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
profileId: string;
|
||||||
|
pkiCollectionId?: string;
|
||||||
|
friendlyName?: string;
|
||||||
|
commonName: string;
|
||||||
|
organization?: string;
|
||||||
|
organizationUnit?: string;
|
||||||
|
locality?: string;
|
||||||
|
state?: string;
|
||||||
|
country?: string;
|
||||||
|
email?: string;
|
||||||
|
streetAddress?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
altNames: string;
|
||||||
|
ttl: string;
|
||||||
|
notBefore?: string;
|
||||||
|
notAfter?: string;
|
||||||
|
keyUsages: CertKeyUsage[];
|
||||||
|
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||||
|
signatureAlgorithm?: string;
|
||||||
|
keyAlgorithm?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateCertificateV3Response = TCreateCertificateResponse;
|
||||||
|
|
||||||
export type TRenewCaDTO = {
|
export type TRenewCaDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
caId: string;
|
caId: string;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export {
|
||||||
|
useCreateCertificateProfile,
|
||||||
|
useDeleteCertificateProfile,
|
||||||
|
useUpdateCertificateProfile
|
||||||
|
} from "./mutations";
|
||||||
|
export {
|
||||||
|
certificateProfileKeys,
|
||||||
|
useGetCertificateProfileById,
|
||||||
|
useGetCertificateProfileBySlug,
|
||||||
|
useGetProfileCertificates,
|
||||||
|
useGetProfileMetrics,
|
||||||
|
useListCertificateProfiles
|
||||||
|
} from "./queries";
|
||||||
|
export type * from "./types";
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { certificateProfileKeys } from "./queries";
|
||||||
|
import {
|
||||||
|
TCertificateProfile,
|
||||||
|
TCreateCertificateProfileDTO,
|
||||||
|
TDeleteCertificateProfileDTO,
|
||||||
|
TUpdateCertificateProfileDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateCertificateProfile = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<TCertificateProfile, object, TCreateCertificateProfileDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const { data: response } = await apiRequest.post<{
|
||||||
|
certificateProfile: TCertificateProfile;
|
||||||
|
}>("/api/v1/pki/certificate-profiles", data);
|
||||||
|
return response.certificateProfile;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certificateProfileKeys.list({ projectId })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateCertificateProfile = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<TCertificateProfile, object, TUpdateCertificateProfileDTO>({
|
||||||
|
mutationFn: async ({ profileId, ...data }) => {
|
||||||
|
const { data: response } = await apiRequest.patch<{
|
||||||
|
certificateProfile: TCertificateProfile;
|
||||||
|
}>(`/api/v1/pki/certificate-profiles/${profileId}`, data);
|
||||||
|
return response.certificateProfile;
|
||||||
|
},
|
||||||
|
onSuccess: (profile, { profileId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certificateProfileKeys.list({ projectId: profile.projectId })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certificateProfileKeys.getById(profileId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteCertificateProfile = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<TCertificateProfile, object, TDeleteCertificateProfileDTO>({
|
||||||
|
mutationFn: async ({ profileId }) => {
|
||||||
|
const { data: response } = await apiRequest.delete<{
|
||||||
|
certificateProfile: TCertificateProfile;
|
||||||
|
}>(`/api/v1/pki/certificate-profiles/${profileId}`);
|
||||||
|
return response.certificateProfile;
|
||||||
|
},
|
||||||
|
onSuccess: (profile, { profileId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certificateProfileKeys.list({ projectId: profile.projectId })
|
||||||
|
});
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: certificateProfileKeys.getById(profileId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import {
|
||||||
|
TCertificateProfile,
|
||||||
|
TCertificateProfileMetrics,
|
||||||
|
TCertificateProfileWithDetails,
|
||||||
|
TGetCertificateProfileByIdDTO,
|
||||||
|
TGetCertificateProfileBySlugDTO,
|
||||||
|
TGetProfileCertificatesDTO,
|
||||||
|
TGetProfileMetricsDTO,
|
||||||
|
TListCertificateProfilesDTO,
|
||||||
|
TProfileCertificate
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const certificateProfileKeys = {
|
||||||
|
list: (params: {
|
||||||
|
projectId: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
includeMetrics?: boolean;
|
||||||
|
expiringDays?: number;
|
||||||
|
}) => ["certificate-profiles", "list", params],
|
||||||
|
getById: (profileId: string) => ["certificate-profiles", "get-by-id", profileId],
|
||||||
|
getBySlug: (projectId: string, slug: string) => [
|
||||||
|
"certificate-profiles",
|
||||||
|
"get-by-slug",
|
||||||
|
projectId,
|
||||||
|
slug
|
||||||
|
],
|
||||||
|
getCertificates: (profileId: string, params?: Omit<TGetProfileCertificatesDTO, "profileId">) => [
|
||||||
|
"certificate-profiles",
|
||||||
|
"certificates",
|
||||||
|
profileId,
|
||||||
|
params
|
||||||
|
],
|
||||||
|
getMetrics: (profileId: string, params?: Omit<TGetProfileMetricsDTO, "profileId">) => [
|
||||||
|
"certificate-profiles",
|
||||||
|
"metrics",
|
||||||
|
profileId,
|
||||||
|
params
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useListCertificateProfiles = ({
|
||||||
|
projectId,
|
||||||
|
limit = 20,
|
||||||
|
offset = 0,
|
||||||
|
search,
|
||||||
|
includeMetrics = false,
|
||||||
|
expiringDays = 7
|
||||||
|
}: TListCertificateProfilesDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certificateProfileKeys.list({
|
||||||
|
projectId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
search,
|
||||||
|
includeMetrics,
|
||||||
|
expiringDays
|
||||||
|
}),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateProfiles: TCertificateProfile[];
|
||||||
|
totalCount: number;
|
||||||
|
}>("/api/v1/pki/certificate-profiles", {
|
||||||
|
params: {
|
||||||
|
projectId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
search,
|
||||||
|
includeMetrics,
|
||||||
|
expiringDays
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCertificateProfileById = ({ profileId }: TGetCertificateProfileByIdDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certificateProfileKeys.getById(profileId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateProfile: TCertificateProfileWithDetails;
|
||||||
|
}>(`/api/v1/pki/certificate-profiles/${profileId}`);
|
||||||
|
return data.certificateProfile;
|
||||||
|
},
|
||||||
|
enabled: Boolean(profileId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCertificateProfileBySlug = ({
|
||||||
|
projectId,
|
||||||
|
slug
|
||||||
|
}: TGetCertificateProfileBySlugDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certificateProfileKeys.getBySlug(projectId, slug),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateProfile: TCertificateProfile;
|
||||||
|
}>(`/api/v1/pki/certificate-profiles/slug/${slug}`, {
|
||||||
|
params: { projectId }
|
||||||
|
});
|
||||||
|
return data.certificateProfile;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId && slug)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetProfileCertificates = ({
|
||||||
|
profileId,
|
||||||
|
offset = 0,
|
||||||
|
limit = 20,
|
||||||
|
status,
|
||||||
|
search
|
||||||
|
}: TGetProfileCertificatesDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certificateProfileKeys.getCertificates(profileId, { offset, limit, status, search }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificates: TProfileCertificate[];
|
||||||
|
}>(`/api/v1/pki/certificate-profiles/${profileId}/certificates`, {
|
||||||
|
params: {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
status,
|
||||||
|
search
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data.certificates;
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
export type TCertificateProfile = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
caId: string;
|
||||||
|
certificateTemplateId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
enrollmentType: "api" | "est";
|
||||||
|
estConfigId?: string;
|
||||||
|
apiConfigId?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
metrics?: TCertificateProfileMetrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||||
|
certificateAuthority?: {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
status: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
certificateTemplate?: {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
estConfig?: {
|
||||||
|
id: string;
|
||||||
|
disableBootstrapCaValidation: boolean;
|
||||||
|
hashedPassphrase: string;
|
||||||
|
encryptedCaChain: any;
|
||||||
|
};
|
||||||
|
apiConfig?: {
|
||||||
|
id: string;
|
||||||
|
autoRenew: boolean;
|
||||||
|
autoRenewDays?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateCertificateProfileDTO = {
|
||||||
|
projectId: string;
|
||||||
|
caId: string;
|
||||||
|
certificateTemplateId: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description?: string;
|
||||||
|
enrollmentType: "api" | "est";
|
||||||
|
estConfig?: {
|
||||||
|
disableBootstrapCaValidation?: boolean;
|
||||||
|
passphrase: string;
|
||||||
|
caChain: string;
|
||||||
|
};
|
||||||
|
apiConfig?: {
|
||||||
|
autoRenew?: boolean;
|
||||||
|
autoRenewDays?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateCertificateProfileDTO = {
|
||||||
|
profileId: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
estConfig?: {
|
||||||
|
disableBootstrapCaValidation?: boolean;
|
||||||
|
passphrase?: string;
|
||||||
|
caChain?: string;
|
||||||
|
};
|
||||||
|
apiConfig?: {
|
||||||
|
autoRenew?: boolean;
|
||||||
|
autoRenewDays?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteCertificateProfileDTO = {
|
||||||
|
profileId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListCertificateProfilesDTO = {
|
||||||
|
projectId: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
search?: string;
|
||||||
|
includeMetrics?: boolean;
|
||||||
|
expiringDays?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCertificateProfileByIdDTO = {
|
||||||
|
profileId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCertificateProfileBySlugDTO = {
|
||||||
|
projectId: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateProfileMetrics = {
|
||||||
|
profileId: string;
|
||||||
|
totalCertificates: number;
|
||||||
|
activeCertificates: number;
|
||||||
|
expiredCertificates: number;
|
||||||
|
expiringCertificates: number;
|
||||||
|
revokedCertificates: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProfileCertificate = {
|
||||||
|
id: string;
|
||||||
|
serialNumber: string;
|
||||||
|
cn: string;
|
||||||
|
status: string;
|
||||||
|
notBefore: Date;
|
||||||
|
notAfter: Date;
|
||||||
|
isRevoked: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetProfileCertificatesDTO = {
|
||||||
|
profileId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
status?: "active" | "expired" | "revoked";
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetProfileMetricsDTO = {
|
||||||
|
profileId: string;
|
||||||
|
expiringDays?: number;
|
||||||
|
};
|
||||||
@@ -7,13 +7,17 @@ import { projectKeys } from "../projects";
|
|||||||
import { certTemplateKeys } from "./queries";
|
import { certTemplateKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TCertificateTemplate,
|
TCertificateTemplate,
|
||||||
|
TCertificateTemplateV2New,
|
||||||
TCreateCertificateTemplateDTO,
|
TCreateCertificateTemplateDTO,
|
||||||
TCreateCertificateTemplateV2DTO,
|
TCreateCertificateTemplateV2DTO,
|
||||||
|
TCreateCertificateTemplateV2NewDTO,
|
||||||
TCreateEstConfigDTO,
|
TCreateEstConfigDTO,
|
||||||
TDeleteCertificateTemplateDTO,
|
TDeleteCertificateTemplateDTO,
|
||||||
TDeleteCertificateTemplateV2DTO,
|
TDeleteCertificateTemplateV2DTO,
|
||||||
|
TDeleteCertificateTemplateV2NewDTO,
|
||||||
TUpdateCertificateTemplateDTO,
|
TUpdateCertificateTemplateDTO,
|
||||||
TUpdateCertificateTemplateV2DTO,
|
TUpdateCertificateTemplateV2DTO,
|
||||||
|
TUpdateCertificateTemplateV2NewDTO,
|
||||||
TUpdateEstConfigDTO
|
TUpdateEstConfigDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -163,3 +167,60 @@ export const useUpdateEstConfig = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateCertificateTemplateV2New = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TCertificateTemplateV2New, object, TCreateCertificateTemplateV2NewDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const { data: response } = await apiRequest.post<{
|
||||||
|
certificateTemplate: TCertificateTemplateV2New;
|
||||||
|
}>("/api/v2/certificate-templates", data);
|
||||||
|
return response.certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certTemplateKeys.listTemplatesV2({ projectId })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateCertificateTemplateV2New = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TCertificateTemplateV2New, object, TUpdateCertificateTemplateV2NewDTO>({
|
||||||
|
mutationFn: async ({ templateId, ...data }) => {
|
||||||
|
const { data: response } = await apiRequest.patch<{
|
||||||
|
certificateTemplate: TCertificateTemplateV2New;
|
||||||
|
}>(`/api/v2/certificate-templates/${templateId}`, data);
|
||||||
|
return response.certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: (template, { templateId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certTemplateKeys.listTemplatesV2({ projectId: template.projectId })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certTemplateKeys.getTemplateV2ById(templateId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteCertificateTemplateV2New = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TCertificateTemplateV2New, object, TDeleteCertificateTemplateV2NewDTO>({
|
||||||
|
mutationFn: async ({ templateId }) => {
|
||||||
|
const { data: response } = await apiRequest.delete<{
|
||||||
|
certificateTemplate: TCertificateTemplateV2New;
|
||||||
|
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||||
|
return response.certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: (template, { templateId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: certTemplateKeys.listTemplatesV2({ projectId: template.projectId })
|
||||||
|
});
|
||||||
|
queryClient.removeQueries({
|
||||||
|
queryKey: certTemplateKeys.getTemplateV2ById(templateId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import {
|
import {
|
||||||
TCertificateTemplate,
|
TCertificateTemplate,
|
||||||
TCertificateTemplateV2,
|
TCertificateTemplateV2,
|
||||||
|
TCertificateTemplateV2New,
|
||||||
TEstConfig,
|
TEstConfig,
|
||||||
TListCertificateTemplatesDTO
|
TGetCertificateTemplateV2ByIdDTO,
|
||||||
|
TListCertificateTemplatesDTO,
|
||||||
|
TListCertificateTemplatesV2DTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const certTemplateKeys = {
|
export const certTemplateKeys = {
|
||||||
@@ -16,7 +19,16 @@ export const certTemplateKeys = {
|
|||||||
projectId,
|
projectId,
|
||||||
el
|
el
|
||||||
],
|
],
|
||||||
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
|
getEstConfig: (id: string) => [{ id }, "cert-template-est-config"],
|
||||||
|
listTemplatesV2: ({
|
||||||
|
projectId,
|
||||||
|
...el
|
||||||
|
}: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
projectId: string;
|
||||||
|
}) => ["list-templates-v2", projectId, el],
|
||||||
|
getTemplateV2ById: (id: string) => ["cert-template-v2", id]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetCertTemplate = (id: string) => {
|
export const useGetCertTemplate = (id: string) => {
|
||||||
@@ -68,3 +80,42 @@ export const useGetEstConfig = (certificateTemplateId: string) => {
|
|||||||
enabled: Boolean(certificateTemplateId)
|
enabled: Boolean(certificateTemplateId)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListCertificateTemplatesV2 = ({
|
||||||
|
projectId,
|
||||||
|
limit = 20,
|
||||||
|
offset = 0
|
||||||
|
}: TListCertificateTemplatesV2DTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certTemplateKeys.listTemplatesV2({ projectId, limit, offset }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateTemplates: TCertificateTemplateV2New[];
|
||||||
|
totalCount: number;
|
||||||
|
}>("/api/v2/certificate-templates", {
|
||||||
|
params: {
|
||||||
|
projectId,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCertificateTemplateV2ById = ({
|
||||||
|
templateId
|
||||||
|
}: TGetCertificateTemplateV2ByIdDTO) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certTemplateKeys.getTemplateV2ById(templateId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
certificateTemplate: TCertificateTemplateV2New;
|
||||||
|
}>(`/api/v2/certificate-templates/${templateId}`);
|
||||||
|
return data.certificateTemplate;
|
||||||
|
},
|
||||||
|
enabled: Boolean(templateId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -121,3 +121,101 @@ export type TListCertificateTemplatesDTO = {
|
|||||||
offset?: number;
|
offset?: number;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCertificateTemplateV2Policy = {
|
||||||
|
attributes: Array<{
|
||||||
|
type:
|
||||||
|
| "common_name"
|
||||||
|
| "organization_name"
|
||||||
|
| "organization_unit"
|
||||||
|
| "locality"
|
||||||
|
| "state"
|
||||||
|
| "country"
|
||||||
|
| "email"
|
||||||
|
| "street_address"
|
||||||
|
| "postal_code";
|
||||||
|
include: "mandatory" | "optional" | "prohibit";
|
||||||
|
value?: string[];
|
||||||
|
}>;
|
||||||
|
keyUsages: {
|
||||||
|
requiredUsages: { all: string[] };
|
||||||
|
optionalUsages: { all: string[] };
|
||||||
|
};
|
||||||
|
extendedKeyUsages: {
|
||||||
|
requiredUsages: { all: string[] };
|
||||||
|
optionalUsages: { all: string[] };
|
||||||
|
};
|
||||||
|
subjectAlternativeNames: Array<{
|
||||||
|
type: "dns_name" | "ip_address" | "email" | "uri";
|
||||||
|
include: "mandatory" | "optional" | "prohibit";
|
||||||
|
value?: string[];
|
||||||
|
}>;
|
||||||
|
validity: {
|
||||||
|
maxDuration: { value: number; unit: "days" | "months" | "years" };
|
||||||
|
minDuration?: { value: number; unit: "days" | "months" | "years" };
|
||||||
|
};
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: string[];
|
||||||
|
defaultAlgorithm: string;
|
||||||
|
};
|
||||||
|
keyAlgorithm: {
|
||||||
|
allowedKeyTypes: string[];
|
||||||
|
defaultKeyType: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateTemplateV2New = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
attributes: any;
|
||||||
|
keyUsages: any;
|
||||||
|
extendedKeyUsages: any;
|
||||||
|
subjectAlternativeNames: any;
|
||||||
|
validity: any;
|
||||||
|
signatureAlgorithm: any;
|
||||||
|
keyAlgorithm: any;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateCertificateTemplateV2NewDTO = {
|
||||||
|
projectId: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
attributes: TCertificateTemplateV2Policy["attributes"];
|
||||||
|
keyUsages: TCertificateTemplateV2Policy["keyUsages"];
|
||||||
|
extendedKeyUsages: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||||
|
subjectAlternativeNames: TCertificateTemplateV2Policy["subjectAlternativeNames"];
|
||||||
|
validity: TCertificateTemplateV2Policy["validity"];
|
||||||
|
signatureAlgorithm: TCertificateTemplateV2Policy["signatureAlgorithm"];
|
||||||
|
keyAlgorithm: TCertificateTemplateV2Policy["keyAlgorithm"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateCertificateTemplateV2NewDTO = {
|
||||||
|
templateId: string;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
attributes?: TCertificateTemplateV2Policy["attributes"];
|
||||||
|
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||||
|
extendedKeyUsages?: TCertificateTemplateV2Policy["extendedKeyUsages"];
|
||||||
|
subjectAlternativeNames?: TCertificateTemplateV2Policy["subjectAlternativeNames"];
|
||||||
|
validity?: TCertificateTemplateV2Policy["validity"];
|
||||||
|
signatureAlgorithm?: TCertificateTemplateV2Policy["signatureAlgorithm"];
|
||||||
|
keyAlgorithm?: TCertificateTemplateV2Policy["keyAlgorithm"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteCertificateTemplateV2NewDTO = {
|
||||||
|
templateId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TListCertificateTemplatesV2DTO = {
|
||||||
|
projectId: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetCertificateTemplateV2ByIdDTO = {
|
||||||
|
templateId: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const getCertStatusBadgeVariant = (status: CertStatus) => {
|
|||||||
|
|
||||||
export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
||||||
[CertKeyAlgorithm.RSA_2048]: "RSA 2048",
|
[CertKeyAlgorithm.RSA_2048]: "RSA 2048",
|
||||||
|
[CertKeyAlgorithm.RSA_3072]: "RSA 3072",
|
||||||
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
|
[CertKeyAlgorithm.RSA_4096]: "RSA 4096",
|
||||||
[CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
|
[CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
|
||||||
[CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384"
|
[CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384"
|
||||||
@@ -31,6 +32,7 @@ export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
|
|||||||
|
|
||||||
export const certKeyAlgorithms = [
|
export const certKeyAlgorithms = [
|
||||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
|
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
|
||||||
|
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_3072], value: CertKeyAlgorithm.RSA_3072 },
|
||||||
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_4096], value: CertKeyAlgorithm.RSA_4096 },
|
{ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_4096], value: CertKeyAlgorithm.RSA_4096 },
|
||||||
{
|
{
|
||||||
label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P256],
|
label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P256],
|
||||||
@@ -96,3 +98,12 @@ export const EXTENDED_KEY_USAGES_OPTIONS = [
|
|||||||
{ value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" },
|
{ value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" },
|
||||||
{ value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" }
|
{ value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" }
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
export const SIGNATURE_ALGORITHMS_OPTIONS = [
|
||||||
|
{ value: "RSA-SHA256", label: "RSA-SHA256" },
|
||||||
|
{ value: "RSA-SHA384", label: "RSA-SHA384" },
|
||||||
|
{ value: "RSA-SHA512", label: "RSA-SHA512" },
|
||||||
|
{ value: "ECDSA-SHA256", label: "ECDSA-SHA256" },
|
||||||
|
{ value: "ECDSA-SHA384", label: "ECDSA-SHA384" },
|
||||||
|
{ value: "ECDSA-SHA512", label: "ECDSA-SHA512" }
|
||||||
|
] as const;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum CertStatus {
|
|||||||
|
|
||||||
export enum CertKeyAlgorithm {
|
export enum CertKeyAlgorithm {
|
||||||
RSA_2048 = "RSA_2048",
|
RSA_2048 = "RSA_2048",
|
||||||
|
RSA_3072 = "RSA_3072",
|
||||||
RSA_4096 = "RSA_4096",
|
RSA_4096 = "RSA_4096",
|
||||||
ECDSA_P256 = "EC_prime256v1",
|
ECDSA_P256 = "EC_prime256v1",
|
||||||
ECDSA_P384 = "EC_secp384r1"
|
ECDSA_P384 = "EC_secp384r1"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export type SubscriptionPlan = {
|
|||||||
gateway: boolean;
|
gateway: boolean;
|
||||||
externalKms: boolean;
|
externalKms: boolean;
|
||||||
pkiEst: boolean;
|
pkiEst: boolean;
|
||||||
|
pkiLegacyTemplates: boolean;
|
||||||
enforceMfa: boolean;
|
enforceMfa: boolean;
|
||||||
enforceGoogleSSO: boolean;
|
enforceGoogleSSO: boolean;
|
||||||
projectTemplates: boolean;
|
projectTemplates: boolean;
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import { motion } from "framer-motion";
|
|||||||
|
|
||||||
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||||
import { useProject, useProjectPermission } from "@app/context";
|
import { useProject, useProjectPermission } from "@app/context";
|
||||||
|
import {
|
||||||
|
useListWorkspaceCertificateTemplates,
|
||||||
|
useListWorkspacePkiSubscribers
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
|
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
|
||||||
|
|
||||||
@@ -27,6 +31,16 @@ export const PkiManagerLayout = () => {
|
|||||||
const { assumedPrivilegeDetails } = useProjectPermission();
|
const { assumedPrivilegeDetails } = useProjectPermission();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data: subscribers = [] } = useListWorkspacePkiSubscribers(currentProject?.id || "");
|
||||||
|
const { data: templatesData } = useListWorkspaceCertificateTemplates({
|
||||||
|
projectId: currentProject?.id || ""
|
||||||
|
});
|
||||||
|
const templates = templatesData?.certificateTemplates || [];
|
||||||
|
|
||||||
|
const hasExistingSubscribers = subscribers.length > 0;
|
||||||
|
const hasExistingTemplates = templates.length > 0;
|
||||||
|
const showLegacySection = hasExistingSubscribers || hasExistingTemplates;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
|
||||||
@@ -48,24 +62,7 @@ export const PkiManagerLayout = () => {
|
|||||||
<Menu>
|
<Menu>
|
||||||
<MenuGroup title="Resources">
|
<MenuGroup title="Resources">
|
||||||
<Link
|
<Link
|
||||||
to="/projects/cert-management/$projectId/subscribers"
|
to="/projects/cert-management/$projectId/policies"
|
||||||
params={{
|
|
||||||
projectId: currentProject.id
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ isActive }) => (
|
|
||||||
<MenuItem isSelected={isActive}>
|
|
||||||
<div className="mx-1 flex gap-2">
|
|
||||||
<div className="w-6">
|
|
||||||
<FontAwesomeIcon icon={faSitemap} />
|
|
||||||
</div>
|
|
||||||
Subscribers
|
|
||||||
</div>
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/projects/cert-management/$projectId/certificate-templates"
|
|
||||||
params={{
|
params={{
|
||||||
projectId: currentProject.id
|
projectId: currentProject.id
|
||||||
}}
|
}}
|
||||||
@@ -76,7 +73,7 @@ export const PkiManagerLayout = () => {
|
|||||||
<div className="w-6">
|
<div className="w-6">
|
||||||
<FontAwesomeIcon icon={faFileLines} />
|
<FontAwesomeIcon icon={faFileLines} />
|
||||||
</div>
|
</div>
|
||||||
Certificate Templates
|
Certificate Policies
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -110,7 +107,7 @@ export const PkiManagerLayout = () => {
|
|||||||
<div className="w-6">
|
<div className="w-6">
|
||||||
<FontAwesomeIcon icon={faStamp} />
|
<FontAwesomeIcon icon={faStamp} />
|
||||||
</div>
|
</div>
|
||||||
Certificates Authority
|
Certificates Authorities
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
@@ -167,6 +164,48 @@ export const PkiManagerLayout = () => {
|
|||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
|
{showLegacySection && (
|
||||||
|
<MenuGroup title="Legacy">
|
||||||
|
{hasExistingSubscribers && (
|
||||||
|
<Link
|
||||||
|
to="/projects/cert-management/$projectId/subscribers"
|
||||||
|
params={{
|
||||||
|
projectId: currentProject.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1 flex gap-2">
|
||||||
|
<div className="w-6">
|
||||||
|
<FontAwesomeIcon icon={faSitemap} />
|
||||||
|
</div>
|
||||||
|
Subscribers
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{hasExistingTemplates && (
|
||||||
|
<Link
|
||||||
|
to="/projects/cert-management/$projectId/certificate-templates"
|
||||||
|
params={{
|
||||||
|
projectId: currentProject.id
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isActive }) => (
|
||||||
|
<MenuItem isSelected={isActive}>
|
||||||
|
<div className="mx-1 flex gap-2">
|
||||||
|
<div className="w-6">
|
||||||
|
<FontAwesomeIcon icon={faFileLines} />
|
||||||
|
</div>
|
||||||
|
Certificate Templates
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</MenuGroup>
|
||||||
|
)}
|
||||||
<MenuGroup title="Others">
|
<MenuGroup title="Others">
|
||||||
<Link
|
<Link
|
||||||
to="/projects/cert-management/$projectId/access-management"
|
to="/projects/cert-management/$projectId/access-management"
|
||||||
|
|||||||
@@ -145,7 +145,13 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
maxPathLength: ca.configuration.maxPathLength
|
maxPathLength: ca.configuration.maxPathLength
|
||||||
? String(ca.configuration.maxPathLength)
|
? String(ca.configuration.maxPathLength)
|
||||||
: "",
|
: "",
|
||||||
keyAlgorithm: ca.configuration.keyAlgorithm
|
keyAlgorithm:
|
||||||
|
ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_2048 ||
|
||||||
|
ca.configuration.keyAlgorithm === CertKeyAlgorithm.RSA_4096 ||
|
||||||
|
ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P256 ||
|
||||||
|
ca.configuration.keyAlgorithm === CertKeyAlgorithm.ECDSA_P384
|
||||||
|
? ca.configuration.keyAlgorithm
|
||||||
|
: CertKeyAlgorithm.RSA_2048
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1018
File diff suppressed because it is too large
Load Diff
+15
-3
@@ -7,22 +7,28 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
|||||||
import {
|
import {
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProject
|
useProject,
|
||||||
|
useSubscription
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { useDeleteCert } from "@app/hooks/api";
|
import { useDeleteCert } from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { CertificateCertModal } from "./CertificateCertModal";
|
import { CertificateCertModal } from "./CertificateCertModal";
|
||||||
import { CertificateImportModal } from "./CertificateImportModal";
|
import { CertificateImportModal } from "./CertificateImportModal";
|
||||||
|
import { CertificateIssuanceModal } from "./CertificateIssuanceModal";
|
||||||
import { CertificateModal } from "./CertificateModal";
|
import { CertificateModal } from "./CertificateModal";
|
||||||
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
import { CertificateRevocationModal } from "./CertificateRevocationModal";
|
||||||
import { CertificatesTable } from "./CertificatesTable";
|
import { CertificatesTable } from "./CertificatesTable";
|
||||||
|
|
||||||
export const CertificatesSection = () => {
|
export const CertificatesSection = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
const { subscription } = useSubscription();
|
||||||
const { mutateAsync: deleteCert } = useDeleteCert();
|
const { mutateAsync: deleteCert } = useDeleteCert();
|
||||||
|
|
||||||
|
const useOldCertificateFlow = subscription.pkiLegacyTemplates;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"certificateIssuance",
|
||||||
"certificate",
|
"certificate",
|
||||||
"certificateImport",
|
"certificateImport",
|
||||||
"certificateCert",
|
"certificateCert",
|
||||||
@@ -73,7 +79,9 @@ export const CertificatesSection = () => {
|
|||||||
colorSchema="primary"
|
colorSchema="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => handlePopUpOpen("certificate")}
|
onClick={() =>
|
||||||
|
handlePopUpOpen(useOldCertificateFlow ? "certificate" : "certificateIssuance")
|
||||||
|
}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
Issue
|
Issue
|
||||||
@@ -83,7 +91,11 @@ export const CertificatesSection = () => {
|
|||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
<CertificatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
{useOldCertificateFlow ? (
|
||||||
|
<CertificateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
) : (
|
||||||
|
<CertificateIssuanceModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
)}
|
||||||
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateImportModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<CertificateRevocationModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
|||||||
+21
-17
@@ -19,6 +19,8 @@ import { PkiSubscribersTable } from "./PkiSubscribersTable";
|
|||||||
export const PkiSubscriberSection = () => {
|
export const PkiSubscriberSection = () => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const projectId = currentProject.id;
|
const projectId = currentProject.id;
|
||||||
|
|
||||||
|
const allowNewSubscriberCreation = false;
|
||||||
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
const { mutateAsync: deletePkiSubscriber } = useDeletePkiSubscriber();
|
||||||
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
const { mutateAsync: updatePkiSubscriber } = useUpdatePkiSubscriber();
|
||||||
|
|
||||||
@@ -100,23 +102,25 @@ export const PkiSubscriberSection = () => {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<ProjectPermissionCan
|
{allowNewSubscriberCreation && (
|
||||||
I={ProjectPermissionPkiSubscriberActions.Create}
|
<ProjectPermissionCan
|
||||||
a={ProjectPermissionSub.PkiSubscribers}
|
I={ProjectPermissionPkiSubscriberActions.Create}
|
||||||
>
|
a={ProjectPermissionSub.PkiSubscribers}
|
||||||
{(isAllowed) => (
|
>
|
||||||
<Button
|
{(isAllowed) => (
|
||||||
colorSchema="primary"
|
<Button
|
||||||
type="submit"
|
colorSchema="primary"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
type="submit"
|
||||||
onClick={() => handlePopUpOpen("pkiSubscriber")}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={!isAllowed}
|
onClick={() => handlePopUpOpen("pkiSubscriber")}
|
||||||
className="ml-4"
|
isDisabled={!isAllowed}
|
||||||
>
|
className="ml-4"
|
||||||
Add Subscriber
|
>
|
||||||
</Button>
|
Add Subscriber
|
||||||
)}
|
</Button>
|
||||||
</ProjectPermissionCan>
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PkiSubscribersTable handlePopUpOpen={handlePopUpOpen} />
|
<PkiSubscribersTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export const PkiTemplateListPage = () => {
|
|||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
const [perPage, setPerPage] = useState(PER_PAGE_INIT);
|
||||||
|
|
||||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
"certificateTemplate",
|
"certificateTemplate",
|
||||||
"deleteTemplate",
|
"deleteTemplate",
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { ContentLoader, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
|
||||||
|
import { CertificateProfilesTab } from "./components/CertificateProfilesTab";
|
||||||
|
import { CertificateTemplatesV2Tab } from "./components/CertificateTemplatesV2Tab";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
CertificateTemplatesV2 = "templates-v2",
|
||||||
|
CertificateProfiles = "profiles"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PoliciesPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const [activeTab, setActiveTab] = useState(TabSections.CertificateProfiles);
|
||||||
|
|
||||||
|
if (!currentProject) {
|
||||||
|
return <ContentLoader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||||
|
<Helmet>
|
||||||
|
<title>{t("common.head-title", { title: "Certificate Policies" })}</title>
|
||||||
|
</Helmet>
|
||||||
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
|
<PageHeader
|
||||||
|
title="Certificate Policies"
|
||||||
|
description="Manage certificate templates and profiles for unified certificate issuance"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as TabSections)}>
|
||||||
|
<TabList className="mb-6 w-full">
|
||||||
|
<div className="flex w-full border-b border-mineshaft-600">
|
||||||
|
<Tab value={TabSections.CertificateProfiles}>Certificate Profiles</Tab>
|
||||||
|
<Tab value={TabSections.CertificateTemplatesV2}>Certificate Templates</Tab>
|
||||||
|
</div>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanel value={TabSections.CertificateProfiles}>
|
||||||
|
<CertificateProfilesTab />
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel value={TabSections.CertificateTemplatesV2}>
|
||||||
|
<CertificateTemplatesV2Tab />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { useProjectPermission } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import {
|
||||||
|
TCertificateProfile,
|
||||||
|
useDeleteCertificateProfile
|
||||||
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
|
|
||||||
|
import { CreateProfileModal } from "./CreateProfileModal";
|
||||||
|
import { EditProfileModal } from "./EditProfileModal";
|
||||||
|
import { ProfileList } from "./ProfileList";
|
||||||
|
|
||||||
|
export const CertificateProfilesTab = () => {
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [selectedProfile, setSelectedProfile] = useState<TCertificateProfile | null>(null);
|
||||||
|
|
||||||
|
const deleteProfile = useDeleteCertificateProfile();
|
||||||
|
|
||||||
|
const canCreateProfile = permission.can(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateProfile = () => {
|
||||||
|
setIsCreateModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditProfile = (profile: TCertificateProfile) => {
|
||||||
|
setSelectedProfile(profile);
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteProfile = (profile: TCertificateProfile) => {
|
||||||
|
setSelectedProfile(profile);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!selectedProfile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteProfile.mutateAsync({
|
||||||
|
profileId: selectedProfile.id
|
||||||
|
});
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setSelectedProfile(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete profile:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-mineshaft-100">Certificate Profiles</h2>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Unified certificate issuance configurations combining CA, template, and enrollment
|
||||||
|
method
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canCreateProfile && (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={handleCreateProfile}
|
||||||
|
>
|
||||||
|
Create Profile
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProfileList onEditProfile={handleEditProfile} onDeleteProfile={handleDeleteProfile} />
|
||||||
|
|
||||||
|
<CreateProfileModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
|
||||||
|
|
||||||
|
{selectedProfile && (
|
||||||
|
<>
|
||||||
|
<EditProfileModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
setSelectedProfile(null);
|
||||||
|
}}
|
||||||
|
profile={selectedProfile}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
title={`Delete Certificate Profile ${selectedProfile.name}?`}
|
||||||
|
onChange={(isOpen) => {
|
||||||
|
setIsDeleteModalOpen(isOpen);
|
||||||
|
if (!isOpen) setSelectedProfile(null);
|
||||||
|
}}
|
||||||
|
deleteKey={selectedProfile.name}
|
||||||
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||||
|
import { useCreateCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||||
|
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
|
|
||||||
|
const schema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().trim().min(1, "Profile name is required"),
|
||||||
|
slug: z.string().trim().min(1, "Profile slug is required"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
enrollmentType: z.enum(["api", "est"]),
|
||||||
|
certificateAuthorityId: z.string().min(1, "Certificate Authority is required"),
|
||||||
|
certificateTemplateId: z.string().min(1, "Certificate Template is required"),
|
||||||
|
estConfig: z
|
||||||
|
.object({
|
||||||
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
|
passphrase: z.string().min(1, "EST passphrase is required"),
|
||||||
|
caChain: z.string().min(1, "EST CA chain is required")
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
apiConfig: z
|
||||||
|
.object({
|
||||||
|
autoRenew: z.boolean().optional(),
|
||||||
|
autoRenewDays: z.number().min(1).max(365).optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.enrollmentType === "est" && !data.estConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (data.enrollmentType === "api" && !data.apiConfig) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Configuration is required for selected enrollment type"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
|
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
|
||||||
|
const { data: templateData } = useListCertificateTemplatesV2({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
limit: 100,
|
||||||
|
offset: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const createProfile = useCreateCertificateProfile();
|
||||||
|
|
||||||
|
const certificateAuthorities = caData || [];
|
||||||
|
const certificateTemplates = templateData?.certificateTemplates || [];
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
slug: "",
|
||||||
|
description: "",
|
||||||
|
enrollmentType: "api",
|
||||||
|
certificateAuthorityId: "",
|
||||||
|
certificateTemplateId: "",
|
||||||
|
apiConfig: {
|
||||||
|
autoRenew: false,
|
||||||
|
autoRenewDays: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedName = watch("name");
|
||||||
|
const watchedEnrollmentType = watch("enrollmentType");
|
||||||
|
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
||||||
|
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (watchedName && !watch("slug")) {
|
||||||
|
const slug = watchedName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/(^-|-$)/g, "");
|
||||||
|
setValue("slug", slug);
|
||||||
|
}
|
||||||
|
}, [watchedName, setValue, watch]);
|
||||||
|
|
||||||
|
const onFormSubmit = async (data: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentProject?.id) return;
|
||||||
|
|
||||||
|
const payload: any = {
|
||||||
|
projectId: currentProject.id,
|
||||||
|
name: data.name,
|
||||||
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
enrollmentType: data.enrollmentType,
|
||||||
|
caId: data.certificateAuthorityId,
|
||||||
|
certificateTemplateId: data.certificateTemplateId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.enrollmentType === "est" && data.estConfig) {
|
||||||
|
payload.estConfig = data.estConfig;
|
||||||
|
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||||
|
payload.apiConfig = data.apiConfig;
|
||||||
|
}
|
||||||
|
await createProfile.mutateAsync(payload);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Certificate profile created successfully",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating profile:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create certificate profile",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Create Certificate Profile"
|
||||||
|
subTitle="Configure a new certificate profile for unified certificate issuance"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Profile Name"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Enter profile name" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="slug"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Profile Slug"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="auto-generated-from-name" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Description" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<TextArea {...field} placeholder="Enter profile description" rows={3} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="certificateAuthorityId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Certificate Authority"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
placeholder="Select a certificate authority"
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{certificateAuthorities.map((ca: any) => (
|
||||||
|
<SelectItem key={ca.id} value={ca.id}>
|
||||||
|
{ca.friendlyName || ca.name || ca.commonName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="certificateTemplateId"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Certificate Template"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
onChange(value);
|
||||||
|
if (watchedEnrollmentType === "est") {
|
||||||
|
setValue("estConfig", {
|
||||||
|
disableBootstrapCaValidation: false,
|
||||||
|
passphrase: "",
|
||||||
|
caChain: ""
|
||||||
|
});
|
||||||
|
setValue("apiConfig", undefined);
|
||||||
|
} else {
|
||||||
|
setValue("apiConfig", {
|
||||||
|
autoRenew: false,
|
||||||
|
autoRenewDays: 30
|
||||||
|
});
|
||||||
|
setValue("estConfig", undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Select a certificate template"
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{certificateTemplates.map((template) => (
|
||||||
|
<SelectItem key={template.id} value={template.id}>
|
||||||
|
{template.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="enrollmentType"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Enrollment Type"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select {...field} onValueChange={onChange} className="w-full" position="popper">
|
||||||
|
<SelectItem value="api">API</SelectItem>
|
||||||
|
<SelectItem value="est">EST</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* EST Configuration */}
|
||||||
|
{watchedEnrollmentType === "est" && (
|
||||||
|
<div className="mb-4 space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="estConfig.disableBootstrapCaValidation"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<Checkbox
|
||||||
|
id="disableBootstrapCaValidation"
|
||||||
|
isChecked={value}
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="disableBootstrapCaValidation"
|
||||||
|
className="text-sm font-medium text-mineshaft-100"
|
||||||
|
>
|
||||||
|
Disable Bootstrap CA Validation
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-bunker-300">
|
||||||
|
Skip CA certificate validation during EST bootstrap phase
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="estConfig.passphrase"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="EST Passphrase"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter secure passphrase for EST authentication"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!watchedDisableBootstrapValidation && (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="estConfig.caChain"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="CA Chain Certificate"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<TextArea
|
||||||
|
{...field}
|
||||||
|
placeholder="-----BEGIN CERTIFICATE----- MIIDXTCCAkWgAwIBAgIJAKoK/heBjcOuMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX ... -----END CERTIFICATE-----"
|
||||||
|
rows={6}
|
||||||
|
className="w-full font-mono text-xs"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-bunker-400">
|
||||||
|
Paste the complete CA certificate chain in PEM format
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Configuration */}
|
||||||
|
{watchedEnrollmentType === "api" && (
|
||||||
|
<div className="mb-4 space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="apiConfig.autoRenew"
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Checkbox id="autoRenew" isChecked={value} onCheckedChange={onChange}>
|
||||||
|
Enable Auto-Renewal
|
||||||
|
</Checkbox>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{watchedAutoRenew && (
|
||||||
|
<div className="mb-4 space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="apiConfig.autoRenewDays"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Auto-Renewal Days"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
placeholder="30"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
className="w-full"
|
||||||
|
isDisabled={!watchedAutoRenew}
|
||||||
|
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || 30)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button type="submit" colorSchema="primary" isLoading={isSubmitting}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose} disabled={isSubmitting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+303
@@ -0,0 +1,303 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||||
|
import {
|
||||||
|
TCertificateProfileWithDetails,
|
||||||
|
useUpdateCertificateProfile
|
||||||
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
|
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
profile: TCertificateProfileWithDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EditProfileModal = ({ isOpen, onClose, profile }: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const updateProfile = useUpdateCertificateProfile();
|
||||||
|
|
||||||
|
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
|
||||||
|
const { data: templateData } = useListCertificateTemplatesV2({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
limit: 100,
|
||||||
|
offset: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const certificateAuthorities = caData || [];
|
||||||
|
const certificateTemplates = templateData?.certificateTemplates || [];
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
slug: "",
|
||||||
|
description: "",
|
||||||
|
enrollmentType: "api" as "api" | "est",
|
||||||
|
certificateAuthorityId: "",
|
||||||
|
certificateTemplateId: "",
|
||||||
|
estConfig: {
|
||||||
|
disableBootstrapCaValidation: false,
|
||||||
|
passphrase: "",
|
||||||
|
caChain: ""
|
||||||
|
},
|
||||||
|
apiConfig: {
|
||||||
|
autoRenew: false,
|
||||||
|
autoRenewDays: 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (profile) {
|
||||||
|
setFormData({
|
||||||
|
name: profile.name,
|
||||||
|
slug: profile.slug,
|
||||||
|
description: profile.description || "",
|
||||||
|
enrollmentType: profile.enrollmentType,
|
||||||
|
certificateAuthorityId: profile.caId,
|
||||||
|
certificateTemplateId: profile.certificateTemplateId,
|
||||||
|
estConfig: {
|
||||||
|
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
|
passphrase: "",
|
||||||
|
caChain: ""
|
||||||
|
},
|
||||||
|
apiConfig: {
|
||||||
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||||
|
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string | boolean | number) => {
|
||||||
|
if (field.includes(".")) {
|
||||||
|
const [parent, child] = field.split(".");
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[parent]: {
|
||||||
|
...(prev as any)[parent],
|
||||||
|
[child]: value
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formData.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: any = {
|
||||||
|
profileId: profile.id,
|
||||||
|
name: formData.name,
|
||||||
|
description: formData.description
|
||||||
|
};
|
||||||
|
|
||||||
|
if (formData.enrollmentType === "est") {
|
||||||
|
payload.estConfig = {
|
||||||
|
disableBootstrapCaValidation: formData.estConfig.disableBootstrapCaValidation,
|
||||||
|
passphrase: formData.estConfig.passphrase,
|
||||||
|
caChain: formData.estConfig.caChain
|
||||||
|
};
|
||||||
|
} else if (formData.enrollmentType === "api") {
|
||||||
|
payload.apiConfig = {
|
||||||
|
autoRenew: formData.apiConfig.autoRenew,
|
||||||
|
autoRenewDays: formData.apiConfig.autoRenewDays
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProfile.mutateAsync(payload);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Certificate profile updated successfully",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating profile:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update certificate profile",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onClose}>
|
||||||
|
<ModalContent
|
||||||
|
title="Edit Certificate Profile"
|
||||||
|
subTitle={`Update configuration for ${profile?.name}`}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<FormControl label="Profile Name" isRequired>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter profile name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Profile Slug" isRequired>
|
||||||
|
<Input
|
||||||
|
placeholder="profile-slug"
|
||||||
|
value={formData.slug}
|
||||||
|
onChange={(e) => handleInputChange("slug", e.target.value)}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Description">
|
||||||
|
<TextArea
|
||||||
|
placeholder="Enter profile description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Enrollment Type">
|
||||||
|
<Select
|
||||||
|
value={formData.enrollmentType}
|
||||||
|
onValueChange={(value) => handleInputChange("enrollmentType", value)}
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
<SelectItem value="api">API - Programmatic certificate enrollment</SelectItem>
|
||||||
|
<SelectItem value="est">EST - RFC 7030 certificate enrollment</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Certificate Authority">
|
||||||
|
<Select
|
||||||
|
value={formData.certificateAuthorityId}
|
||||||
|
onValueChange={(value) => handleInputChange("certificateAuthorityId", value)}
|
||||||
|
placeholder="Select a certificate authority"
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
{certificateAuthorities.map((ca: any) => (
|
||||||
|
<SelectItem key={ca.id} value={ca.id}>
|
||||||
|
{ca.friendlyName || ca.name || ca.commonName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Certificate Template">
|
||||||
|
<Select
|
||||||
|
value={formData.certificateTemplateId}
|
||||||
|
onValueChange={(value) => handleInputChange("certificateTemplateId", value)}
|
||||||
|
placeholder="Select a certificate template"
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
{certificateTemplates.map((template) => (
|
||||||
|
<SelectItem key={template.id} value={template.id}>
|
||||||
|
{template.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{/* EST Configuration */}
|
||||||
|
{formData.enrollmentType === "est" && (
|
||||||
|
<div className="space-y-4 rounded border border-mineshaft-600 p-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
id="disableBootstrapCaValidation"
|
||||||
|
isChecked={formData.estConfig.disableBootstrapCaValidation}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleInputChange("estConfig.disableBootstrapCaValidation", checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Disable Bootstrap CA Validation
|
||||||
|
</Checkbox>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="EST Passphrase" isRequired>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter EST passphrase"
|
||||||
|
value={formData.estConfig.passphrase}
|
||||||
|
onChange={(e) => handleInputChange("estConfig.passphrase", e.target.value)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="CA Chain" isRequired>
|
||||||
|
<TextArea
|
||||||
|
placeholder="Enter CA chain (PEM format)"
|
||||||
|
value={formData.estConfig.caChain}
|
||||||
|
onChange={(e) => handleInputChange("estConfig.caChain", e.target.value)}
|
||||||
|
rows={6}
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* API Configuration */}
|
||||||
|
{formData.enrollmentType === "api" && (
|
||||||
|
<div className="space-y-4 rounded border border-mineshaft-600 p-4">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
id="autoRenew"
|
||||||
|
isChecked={formData.apiConfig.autoRenew}
|
||||||
|
onCheckedChange={(checked) => handleInputChange("apiConfig.autoRenew", checked)}
|
||||||
|
>
|
||||||
|
Enable Auto-Renewal
|
||||||
|
</Checkbox>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Auto-Renewal Days">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="30"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
value={formData.apiConfig.autoRenewDays}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange("apiConfig.autoRenewDays", parseInt(e.target.value, 10) || 30)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
|
isLoading={updateProfile.isPending}
|
||||||
|
disabled={!formData.name}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose} disabled={updateProfile.isPending}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import {
|
||||||
|
TCertificateProfile,
|
||||||
|
useListCertificateProfiles
|
||||||
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
|
|
||||||
|
import { ProfileRow } from "./ProfileRow";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onEditProfile: (profile: TCertificateProfile) => void;
|
||||||
|
onDeleteProfile: (profile: TCertificateProfile) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
|
const { data, isLoading } = useListCertificateProfiles({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
limit: 100,
|
||||||
|
offset: 0,
|
||||||
|
includeMetrics: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const profiles = data?.certificateProfiles || [];
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <TableSkeleton columns={7} innerKey="certificate-profiles" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!profiles || profiles.length === 0) {
|
||||||
|
return <EmptyState title="No Certificate Profiles" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Enrollment Type</Th>
|
||||||
|
<Th>Certificate Authority</Th>
|
||||||
|
<Th>Template</Th>
|
||||||
|
<Th>Certificates</Th>
|
||||||
|
<Th>Created</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{profiles.map((profile) => (
|
||||||
|
<ProfileRow
|
||||||
|
key={profile.id}
|
||||||
|
profile={profile}
|
||||||
|
onEditProfile={onEditProfile}
|
||||||
|
onDeleteProfile={onDeleteProfile}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { faCircleInfo, faEdit, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
Td,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProjectPermission } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { useGetCaById } from "@app/hooks/api/ca/queries";
|
||||||
|
import { TCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
||||||
|
import { useGetCertificateTemplateV2ById } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
profile: TCertificateProfile;
|
||||||
|
onEditProfile: (profile: TCertificateProfile) => void;
|
||||||
|
onDeleteProfile: (profile: TCertificateProfile) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) => {
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const { data: caData } = useGetCaById(profile.caId);
|
||||||
|
|
||||||
|
const { data: templateData } = useGetCertificateTemplateV2ById({
|
||||||
|
templateId: profile.certificateTemplateId
|
||||||
|
});
|
||||||
|
|
||||||
|
const canEditProfile = permission.can(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const canDeleteProfile = permission.can(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const getEnrollmentTypeBadge = (enrollmentType: string) => {
|
||||||
|
const config = {
|
||||||
|
api: { variant: "success" as const, label: "API" },
|
||||||
|
est: { variant: "primary" as const, label: "EST" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const { variant, label } = config[enrollmentType as keyof typeof config] || config.api;
|
||||||
|
|
||||||
|
return <Badge variant={variant}>{label}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={profile.id} className="h-10 transition-colors duration-100 hover:bg-mineshaft-700">
|
||||||
|
<Td>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="font-medium text-mineshaft-100">{profile.name}</div>
|
||||||
|
{profile.description && (
|
||||||
|
<Tooltip content={profile.description}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-bunker-300">{profile.slug}</div>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
||||||
|
<Td className="text-center">
|
||||||
|
<span className="text-sm text-mineshaft-300">
|
||||||
|
{caData?.friendlyName || caData?.commonName || profile.caId}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="text-sm text-mineshaft-300">
|
||||||
|
{templateData?.name || profile.certificateTemplateId}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex gap-2 text-xs">
|
||||||
|
{profile.metrics ? (
|
||||||
|
profile.metrics.totalCertificates === 0 ? (
|
||||||
|
<span className="text-bunker-300">No certificates attached</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{profile.metrics.activeCertificates > 0 && (
|
||||||
|
<span className="text-green-400">
|
||||||
|
{profile.metrics.activeCertificates} active
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{profile.metrics.expiringCertificates > 0 && (
|
||||||
|
<>
|
||||||
|
{profile.metrics.activeCertificates > 0 && (
|
||||||
|
<span className="text-gray-400">•</span>
|
||||||
|
)}
|
||||||
|
<span className="text-yellow-400">
|
||||||
|
{profile.metrics.expiringCertificates} expiring
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{profile.metrics.expiredCertificates > 0 && (
|
||||||
|
<>
|
||||||
|
{(profile.metrics.activeCertificates > 0 ||
|
||||||
|
profile.metrics.expiringCertificates > 0) && (
|
||||||
|
<span className="text-gray-400">•</span>
|
||||||
|
)}
|
||||||
|
<span className="text-red-300">
|
||||||
|
{profile.metrics.expiredCertificates} expired
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{profile.metrics.revokedCertificates > 0 && (
|
||||||
|
<>
|
||||||
|
{(profile.metrics.activeCertificates > 0 ||
|
||||||
|
profile.metrics.expiringCertificates > 0 ||
|
||||||
|
profile.metrics.expiredCertificates > 0) && (
|
||||||
|
<span className="text-gray-400">•</span>
|
||||||
|
)}
|
||||||
|
<span className="text-red-400">
|
||||||
|
{profile.metrics.revokedCertificates} revoked
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="text-bunker-300">No metrics available</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="text-sm text-bunker-300">{formatDate(profile.createdAt)}</span>
|
||||||
|
</Td>
|
||||||
|
<Td className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
{canEditProfile && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEditProfile(profile);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{canDeleteProfile && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteProfile(profile);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
||||||
|
export { CreateProfileModal } from "./CreateProfileModal";
|
||||||
|
export { EditProfileModal } from "./EditProfileModal";
|
||||||
|
export { ProfileList } from "./ProfileList";
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { useProjectPermission } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { useDeleteCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations";
|
||||||
|
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
|
import { CreateTemplateModal } from "./CreateTemplateModal";
|
||||||
|
import { EditTemplateModal } from "./EditTemplateModal";
|
||||||
|
import { TemplateList } from "./TemplateList";
|
||||||
|
|
||||||
|
export const CertificateTemplatesV2Tab = () => {
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||||
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState<TCertificateTemplateV2New | null>(null);
|
||||||
|
|
||||||
|
const deleteTemplateV2 = useDeleteCertificateTemplateV2New();
|
||||||
|
|
||||||
|
const canCreateTemplate = permission.can(
|
||||||
|
ProjectPermissionActions.Create,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateTemplate = () => {
|
||||||
|
setIsCreateModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTemplate = (template: TCertificateTemplateV2New) => {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
setIsEditModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteTemplate = (template: TCertificateTemplateV2New) => {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
setIsDeleteModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!selectedTemplate) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteTemplateV2.mutateAsync({
|
||||||
|
templateId: selectedTemplate.id
|
||||||
|
});
|
||||||
|
setIsDeleteModalOpen(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete template:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-semibold text-mineshaft-100">Certificate Templates</h2>
|
||||||
|
<p className="text-sm text-bunker-300">
|
||||||
|
Define certificate policies, validation rules, and attribute constraints for certificate
|
||||||
|
issuance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canCreateTemplate && (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={handleCreateTemplate}
|
||||||
|
>
|
||||||
|
Create Template
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TemplateList onEditTemplate={handleEditTemplate} onDeleteTemplate={handleDeleteTemplate} />
|
||||||
|
|
||||||
|
<CreateTemplateModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
|
||||||
|
|
||||||
|
{selectedTemplate && (
|
||||||
|
<>
|
||||||
|
<EditTemplateModal
|
||||||
|
isOpen={isEditModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsEditModalOpen(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
}}
|
||||||
|
template={selectedTemplate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
title={`Delete Certificate Template ${selectedTemplate.name}?`}
|
||||||
|
onChange={(isOpen) => {
|
||||||
|
setIsDeleteModalOpen(isOpen);
|
||||||
|
if (!isOpen) setSelectedTemplate(null);
|
||||||
|
}}
|
||||||
|
deleteKey={selectedTemplate.name}
|
||||||
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+699
@@ -0,0 +1,699 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject } from "@app/context";
|
||||||
|
import { useCreateCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations";
|
||||||
|
|
||||||
|
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
|
||||||
|
|
||||||
|
export type FormData = TemplateFormData;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ATTRIBUTE_TYPES = [{ value: "common_name", label: "Common Name (CN)" }];
|
||||||
|
|
||||||
|
const SAN_TYPES = [
|
||||||
|
{ value: "dns_name", label: "DNS Name" },
|
||||||
|
{ value: "ip_address", label: "IP Address" },
|
||||||
|
{ value: "email", label: "Email" },
|
||||||
|
{ value: "uri", label: "URI" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const INCLUDE_TYPES = [
|
||||||
|
{ value: "mandatory", label: "Mandatory", color: "red" },
|
||||||
|
{ value: "optional", label: "Optional", color: "blue" },
|
||||||
|
{ value: "prohibit", label: "Prohibited", color: "gray" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const SIGNATURE_ALGORITHMS = [
|
||||||
|
"SHA256-RSA",
|
||||||
|
"SHA384-RSA",
|
||||||
|
"SHA512-RSA",
|
||||||
|
"SHA256-ECDSA",
|
||||||
|
"SHA384-ECDSA",
|
||||||
|
"SHA512-ECDSA"
|
||||||
|
];
|
||||||
|
|
||||||
|
const KEY_ALGORITHMS = [
|
||||||
|
"RSA-2048",
|
||||||
|
"RSA-3072",
|
||||||
|
"RSA-4096",
|
||||||
|
"ECDSA-P256",
|
||||||
|
"ECDSA-P384",
|
||||||
|
"ECDSA-P521"
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CreateTemplateModal = ({ isOpen, onClose }: Props) => {
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
const createTemplate = useCreateCertificateTemplateV2New();
|
||||||
|
const [activeTab, setActiveTab] = useState<string>("basic");
|
||||||
|
|
||||||
|
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(templateSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
attributes: [],
|
||||||
|
keyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||||
|
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||||
|
subjectAlternativeNames: [],
|
||||||
|
validity: {
|
||||||
|
maxDuration: { value: 365, unit: "days" }
|
||||||
|
},
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: ["SHA256-RSA"],
|
||||||
|
defaultAlgorithm: "SHA256-RSA"
|
||||||
|
},
|
||||||
|
keyAlgorithm: {
|
||||||
|
allowedKeyTypes: ["RSA-2048"],
|
||||||
|
defaultKeyType: "RSA-2048"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedAttributes = watch("attributes") || [];
|
||||||
|
const watchedSans = watch("subjectAlternativeNames") || [];
|
||||||
|
const watchedKeyUsages = watch("keyUsages");
|
||||||
|
const watchedExtendedKeyUsages = watch("extendedKeyUsages");
|
||||||
|
|
||||||
|
const onFormSubmit = async (data: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentProject?.id) return;
|
||||||
|
|
||||||
|
const templateData = {
|
||||||
|
projectId: currentProject.id,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
attributes: data.attributes || [],
|
||||||
|
keyUsages: {
|
||||||
|
requiredUsages: { all: data.keyUsages?.requiredUsages || [] },
|
||||||
|
optionalUsages: { all: data.keyUsages?.optionalUsages || [] }
|
||||||
|
},
|
||||||
|
extendedKeyUsages: {
|
||||||
|
requiredUsages: { all: data.extendedKeyUsages?.requiredUsages || [] },
|
||||||
|
optionalUsages: { all: data.extendedKeyUsages?.optionalUsages || [] }
|
||||||
|
},
|
||||||
|
subjectAlternativeNames: data.subjectAlternativeNames || [],
|
||||||
|
validity: {
|
||||||
|
maxDuration: data.validity?.maxDuration || { value: 365, unit: "days" as const }
|
||||||
|
},
|
||||||
|
signatureAlgorithm: {
|
||||||
|
allowedAlgorithms: data.signatureAlgorithm?.allowedAlgorithms || ["SHA256-RSA"],
|
||||||
|
defaultAlgorithm: data.signatureAlgorithm?.defaultAlgorithm || "SHA256-RSA"
|
||||||
|
},
|
||||||
|
keyAlgorithm: {
|
||||||
|
allowedKeyTypes: data.keyAlgorithm?.allowedKeyTypes || ["RSA-2048"],
|
||||||
|
defaultKeyType: data.keyAlgorithm?.defaultKeyType || "RSA-2048"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await createTemplate.mutateAsync(templateData);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Certificate template created successfully",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
reset();
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating template:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create certificate template",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAttribute = () => {
|
||||||
|
const newAttribute = {
|
||||||
|
type: "common_name" as const,
|
||||||
|
include: "optional" as const,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
setValue("attributes", [...watchedAttributes, newAttribute]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAttribute = (index: number) => {
|
||||||
|
const newAttributes = watchedAttributes.filter((_, i) => i !== index);
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSan = () => {
|
||||||
|
const newSan = {
|
||||||
|
type: "dns_name" as const,
|
||||||
|
include: "optional" as const,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSan = (index: number) => {
|
||||||
|
const newSans = watchedSans.filter((_, i) => i !== index);
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleKeyUsage = (usage: string, type: "required" | "optional") => {
|
||||||
|
const current = watchedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
||||||
|
const otherType = type === "required" ? "optional" : "required";
|
||||||
|
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
||||||
|
const otherList = Array.isArray(current[`${otherType}Usages`])
|
||||||
|
? current[`${otherType}Usages`]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const newOtherList = (otherList || []).filter((u) => u !== usage);
|
||||||
|
const newCurrentList = currentList?.includes(usage)
|
||||||
|
? currentList.filter((u) => u !== usage)
|
||||||
|
: [...(currentList || []), usage];
|
||||||
|
|
||||||
|
setValue("keyUsages", {
|
||||||
|
[`${type}Usages`]: newCurrentList,
|
||||||
|
[`${otherType}Usages`]: newOtherList
|
||||||
|
} as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExtendedKeyUsage = (usage: string, type: "required" | "optional") => {
|
||||||
|
const current = watchedExtendedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
||||||
|
const otherType = type === "required" ? "optional" : "required";
|
||||||
|
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
||||||
|
const otherList = Array.isArray(current[`${otherType}Usages`])
|
||||||
|
? current[`${otherType}Usages`]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const newOtherList = (otherList || []).filter((u) => u !== usage);
|
||||||
|
const newCurrentList = currentList?.includes(usage)
|
||||||
|
? currentList.filter((u) => u !== usage)
|
||||||
|
: [...(currentList || []), usage];
|
||||||
|
|
||||||
|
setValue("extendedKeyUsages", {
|
||||||
|
[`${type}Usages`]: newCurrentList,
|
||||||
|
[`${otherType}Usages`]: newOtherList
|
||||||
|
} as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "basic", label: "Basic Info" },
|
||||||
|
{ id: "attributes", label: "Subject Attributes" },
|
||||||
|
{ id: "san", label: "Subject Alternative Names" },
|
||||||
|
{ id: "usages", label: "Key Usages" },
|
||||||
|
{ id: "constraints", label: "Constraints" }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-4xl"
|
||||||
|
title="Create Certificate Template V2"
|
||||||
|
subTitle="Define comprehensive certificate policies, validation rules, and constraints"
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6">
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="flex border-b border-mineshaft-600">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`border-b-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? "border-primary-500 text-primary-400"
|
||||||
|
: "border-transparent text-bunker-300 hover:text-mineshaft-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="max-h-80 overflow-y-auto">
|
||||||
|
{activeTab === "basic" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Template Name"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Enter template name" className="w-full" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<TextArea {...field} placeholder="Enter template description" rows={3} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "attributes" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={addAttribute}
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Attribute
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{watchedAttributes.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-bunker-300">
|
||||||
|
No subject attributes configured yet. Click "Add Attribute" to get
|
||||||
|
started.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
watchedAttributes.map((attr, index) => (
|
||||||
|
<div
|
||||||
|
key={`attr-${attr.type}`}
|
||||||
|
className="flex items-center gap-3 rounded border border-mineshaft-600 p-3"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={attr.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = { ...attr, type: value as any };
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="w-56"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{ATTRIBUTE_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={attr.include}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = { ...attr, include: value as any };
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="w-36"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{INCLUDE_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Pattern/Value (optional)"
|
||||||
|
value={attr.value?.[0] || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = {
|
||||||
|
...attr,
|
||||||
|
value: e.target.value ? [e.target.value] : []
|
||||||
|
};
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeAttribute(index)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "san" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={addSan}
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add SAN
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{watchedSans.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-bunker-300">
|
||||||
|
No subject alternative names configured yet. Click "Add SAN" to get
|
||||||
|
started.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
watchedSans.map((san, index) => (
|
||||||
|
<div
|
||||||
|
key={`san-${san.type}`}
|
||||||
|
className="flex items-center gap-3 rounded border border-mineshaft-600 p-3"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={san.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newSans = [...watchedSans];
|
||||||
|
newSans[index] = { ...san, type: value as any };
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
}}
|
||||||
|
className="w-36"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{SAN_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={san.include}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newSans = [...watchedSans];
|
||||||
|
newSans[index] = { ...san, include: value as any };
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
}}
|
||||||
|
className="w-36"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{INCLUDE_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Pattern/Value (optional)"
|
||||||
|
value={san.value?.[0] || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newSans = [...watchedSans];
|
||||||
|
newSans[index] = {
|
||||||
|
...san,
|
||||||
|
value: e.target.value ? [e.target.value] : []
|
||||||
|
};
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeSan(index)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "usages" && (
|
||||||
|
<KeyUsagesSection
|
||||||
|
watchedKeyUsages={watchedKeyUsages}
|
||||||
|
watchedExtendedKeyUsages={watchedExtendedKeyUsages}
|
||||||
|
toggleKeyUsage={toggleKeyUsage}
|
||||||
|
toggleExtendedKeyUsage={toggleExtendedKeyUsage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "constraints" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="validity.maxDuration.value"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Max Duration"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
type="number"
|
||||||
|
placeholder="365"
|
||||||
|
className="w-full"
|
||||||
|
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="validity.maxDuration.unit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Unit"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem value="days">Days</SelectItem>
|
||||||
|
<SelectItem value="months">Months</SelectItem>
|
||||||
|
<SelectItem value="years">Years</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signatureAlgorithm.allowedAlgorithms"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Signature Algorithms"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{SIGNATURE_ALGORITHMS.map((alg) => {
|
||||||
|
const isSelected = field.value?.includes(alg);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={alg}
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant={isSelected ? "solid" : "outline"}
|
||||||
|
colorSchema={isSelected ? "primary" : "gray"}
|
||||||
|
onClick={() => {
|
||||||
|
const current = field.value || [];
|
||||||
|
let newValue;
|
||||||
|
if (isSelected) {
|
||||||
|
if (current.length > 1) {
|
||||||
|
newValue = current.filter((a) => a !== alg);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newValue = [...current, alg];
|
||||||
|
}
|
||||||
|
field.onChange(newValue);
|
||||||
|
|
||||||
|
const currentDefault = watch(
|
||||||
|
"signatureAlgorithm.defaultAlgorithm"
|
||||||
|
);
|
||||||
|
if (currentDefault && !newValue.includes(currentDefault)) {
|
||||||
|
setValue(
|
||||||
|
"signatureAlgorithm.defaultAlgorithm",
|
||||||
|
newValue[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{alg}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signatureAlgorithm.defaultAlgorithm"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Default Signature Algorithm"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value || ""}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{(watch("signatureAlgorithm.allowedAlgorithms") || []).map(
|
||||||
|
(alg: string) => (
|
||||||
|
<SelectItem key={alg} value={alg}>
|
||||||
|
{alg}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyAlgorithm.allowedKeyTypes"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Key Algorithms"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{KEY_ALGORITHMS.map((alg) => {
|
||||||
|
const isSelected = field.value?.includes(alg);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={alg}
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant={isSelected ? "solid" : "outline"}
|
||||||
|
colorSchema={isSelected ? "primary" : "gray"}
|
||||||
|
onClick={() => {
|
||||||
|
const current = field.value || [];
|
||||||
|
let newValue;
|
||||||
|
if (isSelected) {
|
||||||
|
if (current.length > 1) {
|
||||||
|
newValue = current.filter((a) => a !== alg);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newValue = [...current, alg];
|
||||||
|
}
|
||||||
|
field.onChange(newValue);
|
||||||
|
|
||||||
|
const currentDefault = watch("keyAlgorithm.defaultKeyType");
|
||||||
|
if (currentDefault && !newValue.includes(currentDefault)) {
|
||||||
|
setValue("keyAlgorithm.defaultKeyType", newValue[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{alg}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyAlgorithm.defaultKeyType"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Default Key Algorithm"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value || ""}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
{(watch("keyAlgorithm.allowedKeyTypes") || []).map((alg: string) => (
|
||||||
|
<SelectItem key={alg} value={alg}>
|
||||||
|
{alg}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button type="submit" colorSchema="primary" isLoading={createTemplate.isPending}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose} disabled={createTemplate.isPending}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+778
@@ -0,0 +1,778 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useUpdateCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/mutations";
|
||||||
|
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
|
import { KeyUsagesSection } from "./shared";
|
||||||
|
|
||||||
|
const attributeSchema = z.object({
|
||||||
|
type: z.enum(["common_name"]),
|
||||||
|
include: z.enum(["mandatory", "optional", "prohibit"]),
|
||||||
|
value: z.array(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const sanSchema = z.object({
|
||||||
|
type: z.enum(["dns_name", "ip_address", "email", "uri"]),
|
||||||
|
include: z.enum(["mandatory", "optional", "prohibit"]),
|
||||||
|
value: z.array(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().trim().min(1, "Template name is required"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
attributes: z.array(attributeSchema).optional(),
|
||||||
|
keyUsages: z
|
||||||
|
.object({
|
||||||
|
requiredUsages: z.array(z.string()),
|
||||||
|
optionalUsages: z.array(z.string())
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
extendedKeyUsages: z
|
||||||
|
.object({
|
||||||
|
requiredUsages: z.array(z.string()),
|
||||||
|
optionalUsages: z.array(z.string())
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
subjectAlternativeNames: z.array(sanSchema).optional(),
|
||||||
|
validity: z
|
||||||
|
.object({
|
||||||
|
maxDuration: z.object({
|
||||||
|
value: z.number().positive(),
|
||||||
|
unit: z.enum(["days", "months", "years"])
|
||||||
|
}),
|
||||||
|
minDuration: z
|
||||||
|
.object({
|
||||||
|
value: z.number().positive(),
|
||||||
|
unit: z.enum(["days", "months", "years"])
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
signatureAlgorithm: z
|
||||||
|
.object({
|
||||||
|
allowedAlgorithms: z.array(z.string()).min(1),
|
||||||
|
defaultAlgorithm: z.string()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.object({
|
||||||
|
allowedKeyTypes: z.array(z.string()).min(1),
|
||||||
|
defaultKeyType: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
template: TCertificateTemplateV2New;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ATTRIBUTE_TYPES = [
|
||||||
|
{ value: "common_name", label: "Common Name (CN)" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const SAN_TYPES = [
|
||||||
|
{ value: "dns_name", label: "DNS Name" },
|
||||||
|
{ value: "ip_address", label: "IP Address" },
|
||||||
|
{ value: "email", label: "Email" },
|
||||||
|
{ value: "uri", label: "URI" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const INCLUDE_TYPES = [
|
||||||
|
{ value: "mandatory", label: "Mandatory", color: "red" },
|
||||||
|
{ value: "optional", label: "Optional", color: "blue" },
|
||||||
|
{ value: "prohibit", label: "Prohibited", color: "gray" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const SIGNATURE_ALGORITHMS = [
|
||||||
|
"SHA256-RSA",
|
||||||
|
"SHA384-RSA",
|
||||||
|
"SHA512-RSA",
|
||||||
|
"SHA256-ECDSA",
|
||||||
|
"SHA384-ECDSA",
|
||||||
|
"SHA512-ECDSA"
|
||||||
|
];
|
||||||
|
|
||||||
|
const KEY_ALGORITHMS = [
|
||||||
|
"RSA-2048",
|
||||||
|
"RSA-3072",
|
||||||
|
"RSA-4096",
|
||||||
|
"ECDSA-P256",
|
||||||
|
"ECDSA-P384",
|
||||||
|
"ECDSA-P521"
|
||||||
|
];
|
||||||
|
|
||||||
|
export const EditTemplateModal = ({ isOpen, onClose, template }: Props) => {
|
||||||
|
const updateTemplate = useUpdateCertificateTemplateV2New();
|
||||||
|
const [activeTab, setActiveTab] = useState<string>("basic");
|
||||||
|
|
||||||
|
const getFormDefaultValues = () => {
|
||||||
|
if (!template) {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
attributes: [],
|
||||||
|
keyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||||
|
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
|
||||||
|
subjectAlternativeNames: [],
|
||||||
|
validity: { maxDuration: { value: 365, unit: "days" as const } },
|
||||||
|
signatureAlgorithm: { allowedAlgorithms: ["SHA256-RSA"], defaultAlgorithm: "SHA256-RSA" },
|
||||||
|
keyAlgorithm: { allowedKeyTypes: ["RSA-2048"], defaultKeyType: "RSA-2048" }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const backendKeyUsages = template.keyUsages || {
|
||||||
|
requiredUsages: { all: [] },
|
||||||
|
optionalUsages: { all: [] }
|
||||||
|
};
|
||||||
|
const backendExtendedKeyUsages = template.extendedKeyUsages || {
|
||||||
|
requiredUsages: { all: [] },
|
||||||
|
optionalUsages: { all: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: template.name,
|
||||||
|
description: template.description || "",
|
||||||
|
attributes: template.attributes || [],
|
||||||
|
keyUsages: {
|
||||||
|
requiredUsages: backendKeyUsages.requiredUsages?.all || [],
|
||||||
|
optionalUsages: backendKeyUsages.optionalUsages?.all || []
|
||||||
|
},
|
||||||
|
extendedKeyUsages: {
|
||||||
|
requiredUsages: backendExtendedKeyUsages.requiredUsages?.all || [],
|
||||||
|
optionalUsages: backendExtendedKeyUsages.optionalUsages?.all || []
|
||||||
|
},
|
||||||
|
subjectAlternativeNames: template.subjectAlternativeNames || [],
|
||||||
|
validity: template.validity || { maxDuration: { value: 365, unit: "days" as const } },
|
||||||
|
signatureAlgorithm: template.signatureAlgorithm || {
|
||||||
|
allowedAlgorithms: ["SHA256-RSA"],
|
||||||
|
defaultAlgorithm: "SHA256-RSA"
|
||||||
|
},
|
||||||
|
keyAlgorithm: template.keyAlgorithm || {
|
||||||
|
allowedKeyTypes: ["RSA-2048"],
|
||||||
|
defaultKeyType: "RSA-2048"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: getFormDefaultValues()
|
||||||
|
});
|
||||||
|
|
||||||
|
const watchedAttributes = watch("attributes") || [];
|
||||||
|
const watchedSans = watch("subjectAlternativeNames") || [];
|
||||||
|
const watchedKeyUsages = watch("keyUsages");
|
||||||
|
const watchedExtendedKeyUsages = watch("extendedKeyUsages");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (template) {
|
||||||
|
reset(getFormDefaultValues());
|
||||||
|
}
|
||||||
|
}, [template, reset]);
|
||||||
|
|
||||||
|
const onFormSubmit = async (data: FormData) => {
|
||||||
|
try {
|
||||||
|
const templateData = {
|
||||||
|
templateId: template.id,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
attributes: data.attributes || [],
|
||||||
|
keyUsages: {
|
||||||
|
requiredUsages: { all: data.keyUsages?.requiredUsages || [] },
|
||||||
|
optionalUsages: { all: data.keyUsages?.optionalUsages || [] }
|
||||||
|
},
|
||||||
|
extendedKeyUsages: {
|
||||||
|
requiredUsages: { all: data.extendedKeyUsages?.requiredUsages || [] },
|
||||||
|
optionalUsages: { all: data.extendedKeyUsages?.optionalUsages || [] }
|
||||||
|
},
|
||||||
|
subjectAlternativeNames: data.subjectAlternativeNames || [],
|
||||||
|
validity: data.validity || {
|
||||||
|
maxDuration: { value: 365, unit: "days" as const }
|
||||||
|
},
|
||||||
|
signatureAlgorithm: data.signatureAlgorithm || {
|
||||||
|
allowedAlgorithms: ["SHA256-RSA"],
|
||||||
|
defaultAlgorithm: "SHA256-RSA"
|
||||||
|
},
|
||||||
|
keyAlgorithm: data.keyAlgorithm || {
|
||||||
|
allowedKeyTypes: ["RSA-2048"],
|
||||||
|
defaultKeyType: "RSA-2048"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateTemplate.mutateAsync(templateData);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Certificate template updated successfully",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating template:", error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update certificate template",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addAttribute = () => {
|
||||||
|
const newAttribute = {
|
||||||
|
type: "common_name" as const,
|
||||||
|
include: "optional" as const,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
setValue("attributes", [...watchedAttributes, newAttribute]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAttribute = (index: number) => {
|
||||||
|
const newAttributes = watchedAttributes.filter((_, i) => i !== index);
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addSan = () => {
|
||||||
|
const newSan = {
|
||||||
|
type: "dns_name" as const,
|
||||||
|
include: "optional" as const,
|
||||||
|
value: []
|
||||||
|
};
|
||||||
|
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSan = (index: number) => {
|
||||||
|
const newSans = watchedSans.filter((_, i) => i !== index);
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleKeyUsage = (usage: string, type: "required" | "optional") => {
|
||||||
|
const current = watchedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
||||||
|
const otherType = type === "required" ? "optional" : "required";
|
||||||
|
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
||||||
|
const otherList = Array.isArray(current[`${otherType}Usages`])
|
||||||
|
? current[`${otherType}Usages`]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const newOtherList = otherList.filter((u) => u !== usage);
|
||||||
|
|
||||||
|
const newCurrentList = currentList.includes(usage)
|
||||||
|
? currentList.filter((u) => u !== usage)
|
||||||
|
: [...currentList, usage];
|
||||||
|
|
||||||
|
setValue("keyUsages", {
|
||||||
|
[`${type}Usages`]: newCurrentList,
|
||||||
|
[`${otherType}Usages`]: newOtherList
|
||||||
|
} as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleExtendedKeyUsage = (usage: string, type: "required" | "optional") => {
|
||||||
|
const current = watchedExtendedKeyUsages || { requiredUsages: [], optionalUsages: [] };
|
||||||
|
const otherType = type === "required" ? "optional" : "required";
|
||||||
|
const currentList = Array.isArray(current[`${type}Usages`]) ? current[`${type}Usages`] : [];
|
||||||
|
const otherList = Array.isArray(current[`${otherType}Usages`])
|
||||||
|
? current[`${otherType}Usages`]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const newOtherList = otherList.filter((u) => u !== usage);
|
||||||
|
|
||||||
|
const newCurrentList = currentList.includes(usage)
|
||||||
|
? currentList.filter((u) => u !== usage)
|
||||||
|
: [...currentList, usage];
|
||||||
|
|
||||||
|
setValue("extendedKeyUsages", {
|
||||||
|
[`${type}Usages`]: newCurrentList,
|
||||||
|
[`${otherType}Usages`]: newOtherList
|
||||||
|
} as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: "basic", label: "Basic Info" },
|
||||||
|
{ id: "attributes", label: "Subject Attributes" },
|
||||||
|
{ id: "san", label: "Subject Alternative Names" },
|
||||||
|
{ id: "usages", label: "Key Usages" },
|
||||||
|
{ id: "constraints", label: "Constraints" }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-4xl"
|
||||||
|
title="Edit Certificate Template"
|
||||||
|
subTitle={`Update configuration for ${template?.name}`}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-6">
|
||||||
|
{/* Tab Navigation */}
|
||||||
|
<div className="flex border-b border-mineshaft-600">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`border-b-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||||
|
activeTab === tab.id
|
||||||
|
? "border-primary-500 text-primary-400"
|
||||||
|
: "border-transparent text-bunker-300 hover:text-mineshaft-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="max-h-80 overflow-y-auto">
|
||||||
|
{activeTab === "basic" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Template Name"
|
||||||
|
isRequired
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Enter template name" className="w-full" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="description"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Description"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<TextArea {...field} placeholder="Enter template description" rows={3} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "attributes" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={addAttribute}
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add Attribute
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{watchedAttributes.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-bunker-300">
|
||||||
|
No subject attributes configured yet. Click "Add Attribute" to get
|
||||||
|
started.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
watchedAttributes.map((attr, index) => (
|
||||||
|
<div
|
||||||
|
key={`attr-${attr.type}`}
|
||||||
|
className="flex items-center gap-3 rounded border border-mineshaft-600 p-3"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={attr.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = { ...attr, type: value as any };
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="w-56"
|
||||||
|
>
|
||||||
|
{ATTRIBUTE_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={attr.include}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = { ...attr, include: value as any };
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="w-36"
|
||||||
|
>
|
||||||
|
{INCLUDE_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Pattern/Value (optional)"
|
||||||
|
value={attr.value?.[0] || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newAttributes = [...watchedAttributes];
|
||||||
|
newAttributes[index] = {
|
||||||
|
...attr,
|
||||||
|
value: e.target.value ? [e.target.value] : []
|
||||||
|
};
|
||||||
|
setValue("attributes", newAttributes);
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeAttribute(index)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "san" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={addSan}
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
>
|
||||||
|
Add SAN
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{watchedSans.length === 0 ? (
|
||||||
|
<div className="py-8 text-center text-bunker-300">
|
||||||
|
No subject alternative names configured yet. Click "Add SAN" to get
|
||||||
|
started.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
watchedSans.map((san, index) => (
|
||||||
|
<div
|
||||||
|
key={`san-${san.type}`}
|
||||||
|
className="flex items-center gap-3 rounded border border-mineshaft-600 p-3"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={san.type}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newSans = [...watchedSans];
|
||||||
|
newSans[index] = { ...san, type: value as any };
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
}}
|
||||||
|
className="w-36"
|
||||||
|
>
|
||||||
|
{SAN_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={san.include}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const newSans = [...watchedSans];
|
||||||
|
newSans[index] = { ...san, include: value as any };
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
}}
|
||||||
|
className="w-36"
|
||||||
|
>
|
||||||
|
{INCLUDE_TYPES.map((type) => (
|
||||||
|
<SelectItem key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Pattern/Value (optional)"
|
||||||
|
value={san.value?.[0] || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newSans = [...watchedSans];
|
||||||
|
newSans[index] = {
|
||||||
|
...san,
|
||||||
|
value: e.target.value ? [e.target.value] : []
|
||||||
|
};
|
||||||
|
setValue("subjectAlternativeNames", newSans);
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeSan(index)}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
colorSchema="danger"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "usages" && (
|
||||||
|
<KeyUsagesSection
|
||||||
|
watchedKeyUsages={watchedKeyUsages}
|
||||||
|
watchedExtendedKeyUsages={watchedExtendedKeyUsages}
|
||||||
|
toggleKeyUsage={toggleKeyUsage}
|
||||||
|
toggleExtendedKeyUsage={toggleExtendedKeyUsage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "constraints" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="validity.maxDuration.value"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Max Duration"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="number" placeholder="365" className="w-full" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="validity.maxDuration.unit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Unit"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select {...field} onValueChange={field.onChange} className="w-full">
|
||||||
|
<SelectItem value="days">Days</SelectItem>
|
||||||
|
<SelectItem value="months">Months</SelectItem>
|
||||||
|
<SelectItem value="years">Years</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signatureAlgorithm.allowedAlgorithms"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Signature Algorithms"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{SIGNATURE_ALGORITHMS.map((alg) => {
|
||||||
|
const isSelected = field.value?.includes(alg);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={alg}
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant={isSelected ? "solid" : "outline"}
|
||||||
|
colorSchema={isSelected ? "primary" : "gray"}
|
||||||
|
onClick={() => {
|
||||||
|
const current = field.value || [];
|
||||||
|
let newValue;
|
||||||
|
if (isSelected) {
|
||||||
|
if (current.length > 1) {
|
||||||
|
newValue = current.filter((a) => a !== alg);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newValue = [...current, alg];
|
||||||
|
}
|
||||||
|
field.onChange(newValue);
|
||||||
|
|
||||||
|
const currentDefault = watch(
|
||||||
|
"signatureAlgorithm.defaultAlgorithm"
|
||||||
|
);
|
||||||
|
if (!newValue.includes(currentDefault)) {
|
||||||
|
setValue(
|
||||||
|
"signatureAlgorithm.defaultAlgorithm",
|
||||||
|
newValue[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{alg}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="signatureAlgorithm.defaultAlgorithm"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Default Signature Algorithm"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value || ""}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(watch("signatureAlgorithm.allowedAlgorithms") || []).map(
|
||||||
|
(alg: string) => (
|
||||||
|
<SelectItem key={alg} value={alg}>
|
||||||
|
{alg}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyAlgorithm.allowedKeyTypes"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Allowed Key Algorithms"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{KEY_ALGORITHMS.map((alg) => {
|
||||||
|
const isSelected = field.value?.includes(alg);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={alg}
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant={isSelected ? "solid" : "outline"}
|
||||||
|
colorSchema={isSelected ? "primary" : "gray"}
|
||||||
|
onClick={() => {
|
||||||
|
const current = field.value || [];
|
||||||
|
let newValue;
|
||||||
|
if (isSelected) {
|
||||||
|
if (current.length > 1) {
|
||||||
|
newValue = current.filter((a) => a !== alg);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newValue = [...current, alg];
|
||||||
|
}
|
||||||
|
field.onChange(newValue);
|
||||||
|
|
||||||
|
const currentDefault = watch("keyAlgorithm.defaultKeyType");
|
||||||
|
if (!newValue.includes(currentDefault)) {
|
||||||
|
setValue("keyAlgorithm.defaultKeyType", newValue[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{alg}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyAlgorithm.defaultKeyType"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Default Key Algorithm"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
value={field.value || ""}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(watch("keyAlgorithm.allowedKeyTypes") || []).map((alg: string) => (
|
||||||
|
<SelectItem key={alg} value={alg}>
|
||||||
|
{alg}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Button type="submit" colorSchema="primary" isLoading={updateTemplate.isPending}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline_bg" onClick={onClose} disabled={updateTemplate.isPending}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+137
@@ -0,0 +1,137 @@
|
|||||||
|
import { faCircleInfo, faEdit, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useProject, useProjectPermission } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
|
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onEditTemplate: (template: TCertificateTemplateV2New) => void;
|
||||||
|
onDeleteTemplate: (template: TCertificateTemplateV2New) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
|
const { data, isLoading } = useListCertificateTemplatesV2({
|
||||||
|
projectId: currentProject?.id || "",
|
||||||
|
limit: 100,
|
||||||
|
offset: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const templates = data?.certificateTemplates || [];
|
||||||
|
|
||||||
|
const canEditTemplate = permission.can(
|
||||||
|
ProjectPermissionActions.Edit,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const canDeleteTemplate = permission.can(
|
||||||
|
ProjectPermissionActions.Delete,
|
||||||
|
ProjectPermissionSub.CertificateAuthorities
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <TableSkeleton columns={4} innerKey="certificate-templates" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templates || templates.length === 0) {
|
||||||
|
return <EmptyState title="No Certificate Templates" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Created</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{templates.map((template) => (
|
||||||
|
<Tr
|
||||||
|
key={template.id}
|
||||||
|
className="h-10 transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="font-medium">{template.name}</div>
|
||||||
|
{template.description && (
|
||||||
|
<Tooltip content={template.description}>
|
||||||
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="text-sm text-bunker-300">{formatDate(template.createdAt)}</span>
|
||||||
|
</Td>
|
||||||
|
<Td className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
{canEditTemplate && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onEditTemplate(template);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
>
|
||||||
|
Edit Template
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
{canDeleteTemplate && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteTemplate(template);
|
||||||
|
}}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
>
|
||||||
|
Delete Template
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||||
|
export { CreateTemplateModal } from "./CreateTemplateModal";
|
||||||
|
export { EditTemplateModal } from "./EditTemplateModal";
|
||||||
|
export { TemplateList } from "./TemplateList";
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
import { Button } from "@app/components/v2";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EXTENDED_KEY_USAGES,
|
||||||
|
formatUsageName,
|
||||||
|
getUsageState,
|
||||||
|
KEY_USAGES,
|
||||||
|
toggleUsageState
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
type UsageToggleProps = {
|
||||||
|
value: "required" | "optional" | undefined;
|
||||||
|
onChange: (value: "required" | "optional" | undefined) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UsageToggle = ({ value, onChange }: UsageToggleProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-x-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(value === "required" ? undefined : "required");
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
className={`${
|
||||||
|
value === "required" ? "bg-mineshaft-500" : "bg-transparent"
|
||||||
|
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||||
|
>
|
||||||
|
Required
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(value === "optional" ? undefined : "optional");
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
className={`${
|
||||||
|
value === "optional" ? "bg-mineshaft-500" : "bg-transparent"
|
||||||
|
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||||
|
>
|
||||||
|
Optional
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type KeyUsagesSectionProps = {
|
||||||
|
watchedKeyUsages?: {
|
||||||
|
requiredUsages?: string[];
|
||||||
|
optionalUsages?: string[];
|
||||||
|
};
|
||||||
|
watchedExtendedKeyUsages?: {
|
||||||
|
requiredUsages?: string[];
|
||||||
|
optionalUsages?: string[];
|
||||||
|
};
|
||||||
|
toggleKeyUsage: (usage: string, type: "required" | "optional") => void;
|
||||||
|
toggleExtendedKeyUsage: (usage: string, type: "required" | "optional") => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KeyUsagesSection = ({
|
||||||
|
watchedKeyUsages,
|
||||||
|
watchedExtendedKeyUsages,
|
||||||
|
toggleKeyUsage,
|
||||||
|
toggleExtendedKeyUsage
|
||||||
|
}: KeyUsagesSectionProps) => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-mineshaft-200">Key Usages</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{KEY_USAGES.map((usage) => {
|
||||||
|
const requiredUsages = Array.isArray(watchedKeyUsages?.requiredUsages)
|
||||||
|
? watchedKeyUsages.requiredUsages
|
||||||
|
: [];
|
||||||
|
const optionalUsages = Array.isArray(watchedKeyUsages?.optionalUsages)
|
||||||
|
? watchedKeyUsages.optionalUsages
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const currentState = getUsageState(usage, requiredUsages, optionalUsages);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={usage} className="flex items-center justify-between p-2">
|
||||||
|
<span className="text-sm capitalize text-mineshaft-300">
|
||||||
|
{formatUsageName(usage)}
|
||||||
|
</span>
|
||||||
|
<UsageToggle
|
||||||
|
value={currentState}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
toggleUsageState(
|
||||||
|
usage,
|
||||||
|
newValue,
|
||||||
|
requiredUsages,
|
||||||
|
optionalUsages,
|
||||||
|
(u) => toggleKeyUsage(u, "required"),
|
||||||
|
(u) => toggleKeyUsage(u, "optional")
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-mineshaft-200">Extended Key Usages</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{EXTENDED_KEY_USAGES.map((usage) => {
|
||||||
|
const requiredUsages = Array.isArray(watchedExtendedKeyUsages?.requiredUsages)
|
||||||
|
? watchedExtendedKeyUsages.requiredUsages
|
||||||
|
: [];
|
||||||
|
const optionalUsages = Array.isArray(watchedExtendedKeyUsages?.optionalUsages)
|
||||||
|
? watchedExtendedKeyUsages.optionalUsages
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const currentState = getUsageState(usage, requiredUsages, optionalUsages);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={usage} className="flex items-center justify-between p-2">
|
||||||
|
<span className="text-sm capitalize text-mineshaft-300">
|
||||||
|
{formatUsageName(usage)}
|
||||||
|
</span>
|
||||||
|
<UsageToggle
|
||||||
|
value={currentState}
|
||||||
|
onChange={(newValue) => {
|
||||||
|
toggleUsageState(
|
||||||
|
usage,
|
||||||
|
newValue,
|
||||||
|
requiredUsages,
|
||||||
|
optionalUsages,
|
||||||
|
(u) => toggleExtendedKeyUsage(u, "required"),
|
||||||
|
(u) => toggleExtendedKeyUsage(u, "optional")
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./components";
|
||||||
|
export * from "./schemas";
|
||||||
|
export * from "./utils";
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { INCLUDE_OPTIONS, SAN_TYPES, SUBJECT_ATTRIBUTE_TYPES } from "./utils";
|
||||||
|
|
||||||
|
export const attributeSchema = z.object({
|
||||||
|
type: z.enum(SUBJECT_ATTRIBUTE_TYPES),
|
||||||
|
include: z.enum(INCLUDE_OPTIONS),
|
||||||
|
value: z.array(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sanSchema = z.object({
|
||||||
|
type: z.enum(SAN_TYPES),
|
||||||
|
include: z.enum(INCLUDE_OPTIONS),
|
||||||
|
value: z.array(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const templateSchema = z.object({
|
||||||
|
name: z.string().trim().min(1, "Template name is required"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
attributes: z.array(attributeSchema).optional(),
|
||||||
|
keyUsages: z
|
||||||
|
.object({
|
||||||
|
requiredUsages: z.array(z.string()).optional(),
|
||||||
|
optionalUsages: z.array(z.string()).optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
extendedKeyUsages: z
|
||||||
|
.object({
|
||||||
|
requiredUsages: z.array(z.string()).optional(),
|
||||||
|
optionalUsages: z.array(z.string()).optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
subjectAlternativeNames: z.array(sanSchema).optional(),
|
||||||
|
validity: z
|
||||||
|
.object({
|
||||||
|
maxDuration: z
|
||||||
|
.object({
|
||||||
|
value: z.number().min(1, "Duration must be at least 1"),
|
||||||
|
unit: z.enum(["days", "months", "years"])
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
minDuration: z
|
||||||
|
.object({
|
||||||
|
value: z.number().min(1, "Duration must be at least 1"),
|
||||||
|
unit: z.enum(["days", "months", "years"])
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
signatureAlgorithm: z
|
||||||
|
.object({
|
||||||
|
allowedAlgorithms: z.array(z.string()).optional(),
|
||||||
|
defaultAlgorithm: z.string().optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
keyAlgorithm: z
|
||||||
|
.object({
|
||||||
|
allowedKeyTypes: z.array(z.string()).optional(),
|
||||||
|
defaultKeyType: z.string().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TemplateFormData = z.infer<typeof templateSchema>;
|
||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
export const KEY_USAGES = [
|
||||||
|
"digital_signature",
|
||||||
|
"key_encipherment",
|
||||||
|
"non_repudiation",
|
||||||
|
"data_encipherment",
|
||||||
|
"key_agreement",
|
||||||
|
"key_cert_sign",
|
||||||
|
"crl_sign",
|
||||||
|
"encipher_only",
|
||||||
|
"decipher_only"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const EXTENDED_KEY_USAGES = [
|
||||||
|
"client_auth",
|
||||||
|
"server_auth",
|
||||||
|
"code_signing",
|
||||||
|
"email_protection",
|
||||||
|
"ocsp_signing",
|
||||||
|
"time_stamping"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SUBJECT_ATTRIBUTE_TYPES = [
|
||||||
|
"common_name",
|
||||||
|
"organization_name",
|
||||||
|
"organization_unit",
|
||||||
|
"locality",
|
||||||
|
"state",
|
||||||
|
"country",
|
||||||
|
"email",
|
||||||
|
"street_address",
|
||||||
|
"postal_code"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const SAN_TYPES = ["dns_name", "ip_address", "email", "uri"] as const;
|
||||||
|
|
||||||
|
export const INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
|
||||||
|
|
||||||
|
export const formatUsageName = (usage: string): string => {
|
||||||
|
return usage.replace(/_/g, " ");
|
||||||
|
};
|
||||||
|
export const getUsageState = (
|
||||||
|
usage: string,
|
||||||
|
requiredUsages: string[],
|
||||||
|
optionalUsages: string[]
|
||||||
|
): "required" | "optional" | undefined => {
|
||||||
|
if (requiredUsages.includes(usage)) return "required";
|
||||||
|
if (optionalUsages.includes(usage)) return "optional";
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleUsageState = (
|
||||||
|
usage: string,
|
||||||
|
newState: "required" | "optional" | undefined,
|
||||||
|
currentRequiredUsages: string[],
|
||||||
|
currentOptionalUsages: string[],
|
||||||
|
toggleRequired: (usage: string) => void,
|
||||||
|
toggleOptional: (usage: string) => void
|
||||||
|
) => {
|
||||||
|
const isRequired = currentRequiredUsages.includes(usage);
|
||||||
|
const isOptional = currentOptionalUsages.includes(usage);
|
||||||
|
|
||||||
|
if (newState === "required") {
|
||||||
|
if (isOptional) toggleOptional(usage);
|
||||||
|
if (!isRequired) toggleRequired(usage);
|
||||||
|
} else if (newState === "optional") {
|
||||||
|
if (isRequired) toggleRequired(usage);
|
||||||
|
if (!isOptional) toggleOptional(usage);
|
||||||
|
} else {
|
||||||
|
if (isRequired) toggleRequired(usage);
|
||||||
|
if (isOptional) toggleOptional(usage);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
||||||
|
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { PoliciesPage } from "./PoliciesPage";
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { PoliciesPage } from "./PoliciesPage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies"
|
||||||
|
)({
|
||||||
|
component: PoliciesPage,
|
||||||
|
beforeLoad: ({ context }) => {
|
||||||
|
return {
|
||||||
|
breadcrumbs: [
|
||||||
|
...context.breadcrumbs,
|
||||||
|
{
|
||||||
|
label: "Certificate Policies"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -114,6 +114,7 @@ import { Route as kmsSettingsPageRouteImport } from './pages/kms/SettingsPage/ro
|
|||||||
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
|
import { Route as kmsOverviewPageRouteImport } from './pages/kms/OverviewPage/route'
|
||||||
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
|
import { Route as kmsKmipPageRouteImport } from './pages/kms/KmipPage/route'
|
||||||
import { Route as certManagerSettingsPageRouteImport } from './pages/cert-manager/SettingsPage/route'
|
import { Route as certManagerSettingsPageRouteImport } from './pages/cert-manager/SettingsPage/route'
|
||||||
|
import { Route as certManagerPoliciesPageRouteImport } from './pages/cert-manager/PoliciesPage/route'
|
||||||
import { Route as certManagerCertificatesPageRouteImport } from './pages/cert-manager/CertificatesPage/route'
|
import { Route as certManagerCertificatesPageRouteImport } from './pages/cert-manager/CertificatesPage/route'
|
||||||
import { Route as certManagerCertificateAuthoritiesPageRouteImport } from './pages/cert-manager/CertificateAuthoritiesPage/route'
|
import { Route as certManagerCertificateAuthoritiesPageRouteImport } from './pages/cert-manager/CertificateAuthoritiesPage/route'
|
||||||
import { Route as certManagerAlertingPageRouteImport } from './pages/cert-manager/AlertingPage/route'
|
import { Route as certManagerAlertingPageRouteImport } from './pages/cert-manager/AlertingPage/route'
|
||||||
@@ -1216,6 +1217,13 @@ const certManagerSettingsPageRouteRoute =
|
|||||||
getParentRoute: () => certManagerLayoutRoute,
|
getParentRoute: () => certManagerLayoutRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const certManagerPoliciesPageRouteRoute =
|
||||||
|
certManagerPoliciesPageRouteImport.update({
|
||||||
|
id: '/policies',
|
||||||
|
path: '/policies',
|
||||||
|
getParentRoute: () => certManagerLayoutRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const certManagerCertificatesPageRouteRoute =
|
const certManagerCertificatesPageRouteRoute =
|
||||||
certManagerCertificatesPageRouteImport.update({
|
certManagerCertificatesPageRouteImport.update({
|
||||||
id: '/certificates',
|
id: '/certificates',
|
||||||
@@ -2820,6 +2828,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof certManagerCertificatesPageRouteImport
|
preLoaderRoute: typeof certManagerCertificatesPageRouteImport
|
||||||
parentRoute: typeof certManagerLayoutImport
|
parentRoute: typeof certManagerLayoutImport
|
||||||
}
|
}
|
||||||
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies': {
|
||||||
|
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies'
|
||||||
|
path: '/policies'
|
||||||
|
fullPath: '/projects/cert-management/$projectId/policies'
|
||||||
|
preLoaderRoute: typeof certManagerPoliciesPageRouteImport
|
||||||
|
parentRoute: typeof certManagerLayoutImport
|
||||||
|
}
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings': {
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings': {
|
||||||
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings'
|
id: '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings'
|
||||||
path: '/settings'
|
path: '/settings'
|
||||||
@@ -4165,6 +4180,7 @@ interface certManagerLayoutRouteChildren {
|
|||||||
certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute
|
certManagerAlertingPageRouteRoute: typeof certManagerAlertingPageRouteRoute
|
||||||
certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute
|
certManagerCertificateAuthoritiesPageRouteRoute: typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||||
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
|
certManagerCertificatesPageRouteRoute: typeof certManagerCertificatesPageRouteRoute
|
||||||
|
certManagerPoliciesPageRouteRoute: typeof certManagerPoliciesPageRouteRoute
|
||||||
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
|
certManagerSettingsPageRouteRoute: typeof certManagerSettingsPageRouteRoute
|
||||||
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
|
projectAccessControlPageRouteCertManagerRoute: typeof projectAccessControlPageRouteCertManagerRoute
|
||||||
projectAppConnectionsPageRouteCertManagerRoute: typeof projectAppConnectionsPageRouteCertManagerRoute
|
projectAppConnectionsPageRouteCertManagerRoute: typeof projectAppConnectionsPageRouteCertManagerRoute
|
||||||
@@ -4185,6 +4201,7 @@ const certManagerLayoutRouteChildren: certManagerLayoutRouteChildren = {
|
|||||||
certManagerCertificateAuthoritiesPageRouteRoute:
|
certManagerCertificateAuthoritiesPageRouteRoute:
|
||||||
certManagerCertificateAuthoritiesPageRouteRoute,
|
certManagerCertificateAuthoritiesPageRouteRoute,
|
||||||
certManagerCertificatesPageRouteRoute: certManagerCertificatesPageRouteRoute,
|
certManagerCertificatesPageRouteRoute: certManagerCertificatesPageRouteRoute,
|
||||||
|
certManagerPoliciesPageRouteRoute: certManagerPoliciesPageRouteRoute,
|
||||||
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
|
certManagerSettingsPageRouteRoute: certManagerSettingsPageRouteRoute,
|
||||||
projectAccessControlPageRouteCertManagerRoute:
|
projectAccessControlPageRouteCertManagerRoute:
|
||||||
projectAccessControlPageRouteCertManagerRoute,
|
projectAccessControlPageRouteCertManagerRoute,
|
||||||
@@ -5100,6 +5117,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/projects/cert-management/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
'/projects/cert-management/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
||||||
'/projects/cert-management/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
'/projects/cert-management/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||||
'/projects/cert-management/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
|
'/projects/cert-management/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
|
||||||
|
'/projects/cert-management/$projectId/policies': typeof certManagerPoliciesPageRouteRoute
|
||||||
'/projects/cert-management/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
'/projects/cert-management/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
||||||
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||||
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
||||||
@@ -5333,6 +5351,7 @@ export interface FileRoutesByTo {
|
|||||||
'/projects/cert-management/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
'/projects/cert-management/$projectId/alerting': typeof certManagerAlertingPageRouteRoute
|
||||||
'/projects/cert-management/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
'/projects/cert-management/$projectId/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||||
'/projects/cert-management/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
|
'/projects/cert-management/$projectId/certificates': typeof certManagerCertificatesPageRouteRoute
|
||||||
|
'/projects/cert-management/$projectId/policies': typeof certManagerPoliciesPageRouteRoute
|
||||||
'/projects/cert-management/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
'/projects/cert-management/$projectId/settings': typeof certManagerSettingsPageRouteRoute
|
||||||
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
'/projects/kms/$projectId/kmip': typeof kmsKmipPageRouteRoute
|
||||||
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
'/projects/kms/$projectId/overview': typeof kmsOverviewPageRouteRoute
|
||||||
@@ -5578,6 +5597,7 @@ export interface FileRoutesById {
|
|||||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting': typeof certManagerAlertingPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting': typeof certManagerAlertingPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities': typeof certManagerCertificateAuthoritiesPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates': typeof certManagerCertificatesPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates': typeof certManagerCertificatesPageRouteRoute
|
||||||
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies': typeof certManagerPoliciesPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings': typeof certManagerSettingsPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip': typeof kmsKmipPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
|
'/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview': typeof kmsOverviewPageRouteRoute
|
||||||
@@ -5821,6 +5841,7 @@ export interface FileRouteTypes {
|
|||||||
| '/projects/cert-management/$projectId/alerting'
|
| '/projects/cert-management/$projectId/alerting'
|
||||||
| '/projects/cert-management/$projectId/certificate-authorities'
|
| '/projects/cert-management/$projectId/certificate-authorities'
|
||||||
| '/projects/cert-management/$projectId/certificates'
|
| '/projects/cert-management/$projectId/certificates'
|
||||||
|
| '/projects/cert-management/$projectId/policies'
|
||||||
| '/projects/cert-management/$projectId/settings'
|
| '/projects/cert-management/$projectId/settings'
|
||||||
| '/projects/kms/$projectId/kmip'
|
| '/projects/kms/$projectId/kmip'
|
||||||
| '/projects/kms/$projectId/overview'
|
| '/projects/kms/$projectId/overview'
|
||||||
@@ -6053,6 +6074,7 @@ export interface FileRouteTypes {
|
|||||||
| '/projects/cert-management/$projectId/alerting'
|
| '/projects/cert-management/$projectId/alerting'
|
||||||
| '/projects/cert-management/$projectId/certificate-authorities'
|
| '/projects/cert-management/$projectId/certificate-authorities'
|
||||||
| '/projects/cert-management/$projectId/certificates'
|
| '/projects/cert-management/$projectId/certificates'
|
||||||
|
| '/projects/cert-management/$projectId/policies'
|
||||||
| '/projects/cert-management/$projectId/settings'
|
| '/projects/cert-management/$projectId/settings'
|
||||||
| '/projects/kms/$projectId/kmip'
|
| '/projects/kms/$projectId/kmip'
|
||||||
| '/projects/kms/$projectId/overview'
|
| '/projects/kms/$projectId/overview'
|
||||||
@@ -6296,6 +6318,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates'
|
||||||
|
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/kmip'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview'
|
| '/_authenticate/_inject-org-details/_org-layout/projects/kms/$projectId/_kms-layout/overview'
|
||||||
@@ -6943,6 +6966,7 @@ export const routeTree = rootRoute
|
|||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting",
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/alerting",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities",
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates",
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/certificates",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings",
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management",
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/access-management",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections",
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/app-connections",
|
||||||
@@ -7060,6 +7084,10 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "cert-manager/CertificatesPage/route.tsx",
|
"filePath": "cert-manager/CertificatesPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
|
||||||
},
|
},
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/policies": {
|
||||||
|
"filePath": "cert-manager/PoliciesPage/route.tsx",
|
||||||
|
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
|
||||||
|
},
|
||||||
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings": {
|
"/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout/settings": {
|
||||||
"filePath": "cert-manager/SettingsPage/route.tsx",
|
"filePath": "cert-manager/SettingsPage/route.tsx",
|
||||||
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
|
"parent": "/_authenticate/_inject-org-details/_org-layout/projects/cert-management/$projectId/_cert-manager-layout"
|
||||||
|
|||||||
@@ -301,6 +301,7 @@ const secretManagerIntegrationsRedirect = route("/integrations", [
|
|||||||
|
|
||||||
const certManagerRoutes = route("/projects/cert-management/$projectId", [
|
const certManagerRoutes = route("/projects/cert-management/$projectId", [
|
||||||
layout("cert-manager-layout", "cert-manager/layout.tsx", [
|
layout("cert-manager-layout", "cert-manager/layout.tsx", [
|
||||||
|
route("/policies", "cert-manager/PoliciesPage/route.tsx"),
|
||||||
route("/subscribers", [
|
route("/subscribers", [
|
||||||
index("cert-manager/PkiSubscribersPage/route.tsx"),
|
index("cert-manager/PkiSubscribersPage/route.tsx"),
|
||||||
route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx")
|
route("/$subscriberName", "cert-manager/PkiSubscriberDetailsByIDPage/route.tsx")
|
||||||
|
|||||||
Reference in New Issue
Block a user