mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 19:28:09 +00:00
PKI revamp: UI improvements
This commit is contained in:
Vendored
+14
-14
@@ -17,9 +17,9 @@ import {
|
|||||||
TAccessApprovalRequestsReviewersInsert,
|
TAccessApprovalRequestsReviewersInsert,
|
||||||
TAccessApprovalRequestsReviewersUpdate,
|
TAccessApprovalRequestsReviewersUpdate,
|
||||||
TAccessApprovalRequestsUpdate,
|
TAccessApprovalRequestsUpdate,
|
||||||
TApiEnrollmentConfigs,
|
TPkiApiEnrollmentConfigs,
|
||||||
TApiEnrollmentConfigsInsert,
|
TPkiApiEnrollmentConfigsInsert,
|
||||||
TApiEnrollmentConfigsUpdate,
|
TPkiApiEnrollmentConfigsUpdate,
|
||||||
TApiKeys,
|
TApiKeys,
|
||||||
TApiKeysInsert,
|
TApiKeysInsert,
|
||||||
TApiKeysUpdate,
|
TApiKeysUpdate,
|
||||||
@@ -80,9 +80,9 @@ import {
|
|||||||
TDynamicSecrets,
|
TDynamicSecrets,
|
||||||
TDynamicSecretsInsert,
|
TDynamicSecretsInsert,
|
||||||
TDynamicSecretsUpdate,
|
TDynamicSecretsUpdate,
|
||||||
TEstEnrollmentConfigs,
|
TPkiEstEnrollmentConfigs,
|
||||||
TEstEnrollmentConfigsInsert,
|
TPkiEstEnrollmentConfigsInsert,
|
||||||
TEstEnrollmentConfigsUpdate,
|
TPkiEstEnrollmentConfigsUpdate,
|
||||||
TExternalCertificateAuthorities,
|
TExternalCertificateAuthorities,
|
||||||
TExternalCertificateAuthoritiesInsert,
|
TExternalCertificateAuthoritiesInsert,
|
||||||
TExternalCertificateAuthoritiesUpdate,
|
TExternalCertificateAuthoritiesUpdate,
|
||||||
@@ -678,15 +678,15 @@ declare module "knex/types/tables" {
|
|||||||
TCertificateProfilesInsert,
|
TCertificateProfilesInsert,
|
||||||
TCertificateProfilesUpdate
|
TCertificateProfilesUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.EstEnrollmentConfig]: KnexOriginal.CompositeTableType<
|
[TableName.PkiEstEnrollmentConfig]: KnexOriginal.CompositeTableType<
|
||||||
TEstEnrollmentConfigs,
|
TPkiEstEnrollmentConfigs,
|
||||||
TEstEnrollmentConfigsInsert,
|
TPkiEstEnrollmentConfigsInsert,
|
||||||
TEstEnrollmentConfigsUpdate
|
TPkiEstEnrollmentConfigsUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.ApiEnrollmentConfig]: KnexOriginal.CompositeTableType<
|
[TableName.PkiApiEnrollmentConfig]: KnexOriginal.CompositeTableType<
|
||||||
TApiEnrollmentConfigs,
|
TPkiApiEnrollmentConfigs,
|
||||||
TApiEnrollmentConfigsInsert,
|
TPkiApiEnrollmentConfigsInsert,
|
||||||
TApiEnrollmentConfigsUpdate
|
TPkiApiEnrollmentConfigsUpdate
|
||||||
>;
|
>;
|
||||||
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
|
[TableName.CertificateTemplateEstConfig]: KnexOriginal.CompositeTableType<
|
||||||
TCertificateTemplateEstConfigs,
|
TCertificateTemplateEstConfigs,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.string("projectId").notNullable();
|
t.string("projectId").notNullable();
|
||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
|
||||||
t.string("name", 64).notNullable();
|
t.string("slug").notNullable();
|
||||||
t.string("description");
|
t.string("description");
|
||||||
|
|
||||||
t.jsonb("attributes");
|
t.jsonb("attributes");
|
||||||
@@ -22,13 +22,15 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.jsonb("keyAlgorithm");
|
t.jsonb("keyAlgorithm");
|
||||||
|
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.unique(["slug", "projectId"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2);
|
await createOnUpdateTrigger(knex, TableName.CertificateTemplateV2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await knex.schema.hasTable(TableName.EstEnrollmentConfig))) {
|
if (!(await knex.schema.hasTable(TableName.PkiEstEnrollmentConfig))) {
|
||||||
await knex.schema.createTable(TableName.EstEnrollmentConfig, (t) => {
|
await knex.schema.createTable(TableName.PkiEstEnrollmentConfig, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
t.boolean("disableBootstrapCaValidation").defaultTo(false);
|
t.boolean("disableBootstrapCaValidation").defaultTo(false);
|
||||||
@@ -38,11 +40,11 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.EstEnrollmentConfig);
|
await createOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await knex.schema.hasTable(TableName.ApiEnrollmentConfig))) {
|
if (!(await knex.schema.hasTable(TableName.PkiApiEnrollmentConfig))) {
|
||||||
await knex.schema.createTable(TableName.ApiEnrollmentConfig, (t) => {
|
await knex.schema.createTable(TableName.PkiApiEnrollmentConfig, (t) => {
|
||||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
|
||||||
t.boolean("autoRenew").defaultTo(false);
|
t.boolean("autoRenew").defaultTo(false);
|
||||||
@@ -51,7 +53,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.ApiEnrollmentConfig);
|
await createOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await knex.schema.hasTable(TableName.CertificateProfile))) {
|
if (!(await knex.schema.hasTable(TableName.CertificateProfile))) {
|
||||||
@@ -61,25 +63,24 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE");
|
||||||
|
|
||||||
t.uuid("caId").notNullable();
|
t.uuid("caId").notNullable();
|
||||||
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE");
|
t.foreign("caId").references("id").inTable(TableName.CertificateAuthority);
|
||||||
|
|
||||||
t.uuid("certificateTemplateId").notNullable();
|
t.uuid("certificateTemplateId").notNullable();
|
||||||
t.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplateV2).onDelete("CASCADE");
|
t.foreign("certificateTemplateId").references("id").inTable(TableName.CertificateTemplateV2);
|
||||||
|
|
||||||
t.string("name", 64).notNullable();
|
|
||||||
t.string("slug").notNullable();
|
t.string("slug").notNullable();
|
||||||
t.string("description");
|
t.string("description");
|
||||||
t.string("enrollmentType").notNullable().checkIn(["api", "est"]);
|
t.string("enrollmentType").notNullable().checkIn(["api", "est"]);
|
||||||
|
|
||||||
t.uuid("estConfigId");
|
t.uuid("estConfigId");
|
||||||
t.foreign("estConfigId").references("id").inTable(TableName.EstEnrollmentConfig).onDelete("SET NULL");
|
t.foreign("estConfigId").references("id").inTable(TableName.PkiEstEnrollmentConfig).onDelete("SET NULL");
|
||||||
|
|
||||||
t.uuid("apiConfigId");
|
t.uuid("apiConfigId");
|
||||||
t.foreign("apiConfigId").references("id").inTable(TableName.ApiEnrollmentConfig).onDelete("SET NULL");
|
t.foreign("apiConfigId").references("id").inTable(TableName.PkiApiEnrollmentConfig).onDelete("SET NULL");
|
||||||
|
|
||||||
t.timestamps(true, true, true);
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
t.unique(["slug", "projectId"], { indexName: "certificate_profiles_slug_project_id_unique" });
|
t.unique(["slug", "projectId"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
await createOnUpdateTrigger(knex, TableName.CertificateProfile);
|
await createOnUpdateTrigger(knex, TableName.CertificateProfile);
|
||||||
@@ -89,7 +90,7 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
t.uuid("profileId");
|
t.uuid("profileId");
|
||||||
t.foreign("profileId").references("id").inTable(TableName.CertificateProfile).onDelete("SET NULL");
|
t.foreign("profileId").references("id").inTable(TableName.CertificateProfile).onDelete("SET NULL");
|
||||||
t.index("profileId", "idx_certificates_profile_id");
|
t.index("profileId");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +99,7 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (await knex.schema.hasColumn(TableName.Certificate, "profileId")) {
|
if (await knex.schema.hasColumn(TableName.Certificate, "profileId")) {
|
||||||
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
t.dropForeign(["profileId"]);
|
t.dropForeign(["profileId"]);
|
||||||
t.dropIndex("profileId", "idx_certificates_profile_id");
|
t.dropIndex("profileId");
|
||||||
t.dropColumn("profileId");
|
t.dropColumn("profileId");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -106,11 +107,11 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
await knex.schema.dropTableIfExists(TableName.CertificateProfile);
|
await knex.schema.dropTableIfExists(TableName.CertificateProfile);
|
||||||
await dropOnUpdateTrigger(knex, TableName.CertificateProfile);
|
await dropOnUpdateTrigger(knex, TableName.CertificateProfile);
|
||||||
|
|
||||||
await knex.schema.dropTableIfExists(TableName.ApiEnrollmentConfig);
|
await knex.schema.dropTableIfExists(TableName.PkiApiEnrollmentConfig);
|
||||||
await dropOnUpdateTrigger(knex, TableName.ApiEnrollmentConfig);
|
await dropOnUpdateTrigger(knex, TableName.PkiApiEnrollmentConfig);
|
||||||
|
|
||||||
await knex.schema.dropTableIfExists(TableName.EstEnrollmentConfig);
|
await knex.schema.dropTableIfExists(TableName.PkiEstEnrollmentConfig);
|
||||||
await dropOnUpdateTrigger(knex, TableName.EstEnrollmentConfig);
|
await dropOnUpdateTrigger(knex, TableName.PkiEstEnrollmentConfig);
|
||||||
|
|
||||||
await knex.schema.dropTableIfExists(TableName.CertificateTemplateV2);
|
await knex.schema.dropTableIfExists(TableName.CertificateTemplateV2);
|
||||||
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateV2);
|
await dropOnUpdateTrigger(knex, TableName.CertificateTemplateV2);
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const CertificateProfilesSchema = z.object({
|
|||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
name: z.string(),
|
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
enrollmentType: z.string(),
|
enrollmentType: z.string(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const CertificateTemplatesV2Schema = z.object({
|
export const CertificateTemplatesV2Schema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
name: z.string(),
|
slug: z.string(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
attributes: z.unknown().nullable().optional(),
|
attributes: z.unknown().nullable().optional(),
|
||||||
keyUsages: z.unknown().nullable().optional(),
|
keyUsages: z.unknown().nullable().optional(),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export * from "./access-approval-policies-approvers";
|
|||||||
export * from "./access-approval-policies-bypassers";
|
export * from "./access-approval-policies-bypassers";
|
||||||
export * from "./access-approval-requests";
|
export * from "./access-approval-requests";
|
||||||
export * from "./access-approval-requests-reviewers";
|
export * from "./access-approval-requests-reviewers";
|
||||||
export * from "./api-enrollment-configs";
|
|
||||||
export * from "./api-keys";
|
export * from "./api-keys";
|
||||||
export * from "./app-connections";
|
export * from "./app-connections";
|
||||||
export * from "./audit-log-streams";
|
export * from "./audit-log-streams";
|
||||||
@@ -24,7 +23,6 @@ export * from "./certificate-templates-v2";
|
|||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
export * from "./dynamic-secret-leases";
|
export * from "./dynamic-secret-leases";
|
||||||
export * from "./dynamic-secrets";
|
export * from "./dynamic-secrets";
|
||||||
export * from "./est-enrollment-configs";
|
|
||||||
export * from "./external-certificate-authorities";
|
export * from "./external-certificate-authorities";
|
||||||
export * from "./external-group-org-role-mappings";
|
export * from "./external-group-org-role-mappings";
|
||||||
export * from "./external-kms";
|
export * from "./external-kms";
|
||||||
@@ -92,8 +90,10 @@ export * from "./pam-folders";
|
|||||||
export * from "./pam-resources";
|
export * from "./pam-resources";
|
||||||
export * from "./pam-sessions";
|
export * from "./pam-sessions";
|
||||||
export * from "./pki-alerts";
|
export * from "./pki-alerts";
|
||||||
|
export * from "./pki-api-enrollment-configs";
|
||||||
export * from "./pki-collection-items";
|
export * from "./pki-collection-items";
|
||||||
export * from "./pki-collections";
|
export * from "./pki-collections";
|
||||||
|
export * from "./pki-est-enrollment-configs";
|
||||||
export * from "./pki-subscribers";
|
export * from "./pki-subscribers";
|
||||||
export * from "./pki-syncs";
|
export * from "./pki-syncs";
|
||||||
export * from "./project-bots";
|
export * from "./project-bots";
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export enum TableName {
|
|||||||
CertificateTemplate = "certificate_templates",
|
CertificateTemplate = "certificate_templates",
|
||||||
CertificateTemplateV2 = "certificate_templates_v2",
|
CertificateTemplateV2 = "certificate_templates_v2",
|
||||||
CertificateProfile = "certificate_profiles",
|
CertificateProfile = "certificate_profiles",
|
||||||
EstEnrollmentConfig = "est_enrollment_configs",
|
PkiEstEnrollmentConfig = "pki_est_enrollment_configs",
|
||||||
ApiEnrollmentConfig = "api_enrollment_configs",
|
PkiApiEnrollmentConfig = "pki_api_enrollment_configs",
|
||||||
PkiSubscriber = "pki_subscribers",
|
PkiSubscriber = "pki_subscribers",
|
||||||
PkiAlert = "pki_alerts",
|
PkiAlert = "pki_alerts",
|
||||||
PkiCollection = "pki_collections",
|
PkiCollection = "pki_collections",
|
||||||
|
|||||||
+6
-4
@@ -7,7 +7,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const ApiEnrollmentConfigsSchema = z.object({
|
export const PkiApiEnrollmentConfigsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
autoRenew: z.boolean().default(false).nullable().optional(),
|
autoRenew: z.boolean().default(false).nullable().optional(),
|
||||||
autoRenewDays: z.number().nullable().optional(),
|
autoRenewDays: z.number().nullable().optional(),
|
||||||
@@ -15,6 +15,8 @@ export const ApiEnrollmentConfigsSchema = z.object({
|
|||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TApiEnrollmentConfigs = z.infer<typeof ApiEnrollmentConfigsSchema>;
|
export type TPkiApiEnrollmentConfigs = z.infer<typeof PkiApiEnrollmentConfigsSchema>;
|
||||||
export type TApiEnrollmentConfigsInsert = Omit<z.input<typeof ApiEnrollmentConfigsSchema>, TImmutableDBKeys>;
|
export type TPkiApiEnrollmentConfigsInsert = Omit<z.input<typeof PkiApiEnrollmentConfigsSchema>, TImmutableDBKeys>;
|
||||||
export type TApiEnrollmentConfigsUpdate = Partial<Omit<z.input<typeof ApiEnrollmentConfigsSchema>, TImmutableDBKeys>>;
|
export type TPkiApiEnrollmentConfigsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof PkiApiEnrollmentConfigsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
+6
-4
@@ -9,7 +9,7 @@ import { zodBuffer } from "@app/lib/zod";
|
|||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const EstEnrollmentConfigsSchema = z.object({
|
export const PkiEstEnrollmentConfigsSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
disableBootstrapCaValidation: z.boolean().default(false).nullable().optional(),
|
disableBootstrapCaValidation: z.boolean().default(false).nullable().optional(),
|
||||||
hashedPassphrase: z.string(),
|
hashedPassphrase: z.string(),
|
||||||
@@ -18,6 +18,8 @@ export const EstEnrollmentConfigsSchema = z.object({
|
|||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TEstEnrollmentConfigs = z.infer<typeof EstEnrollmentConfigsSchema>;
|
export type TPkiEstEnrollmentConfigs = z.infer<typeof PkiEstEnrollmentConfigsSchema>;
|
||||||
export type TEstEnrollmentConfigsInsert = Omit<z.input<typeof EstEnrollmentConfigsSchema>, TImmutableDBKeys>;
|
export type TPkiEstEnrollmentConfigsInsert = Omit<z.input<typeof PkiEstEnrollmentConfigsSchema>, TImmutableDBKeys>;
|
||||||
export type TEstEnrollmentConfigsUpdate = Partial<Omit<z.input<typeof EstEnrollmentConfigsSchema>, TImmutableDBKeys>>;
|
export type TPkiEstEnrollmentConfigsUpdate = Partial<
|
||||||
|
Omit<z.input<typeof PkiEstEnrollmentConfigsSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -352,14 +352,10 @@ export enum EventType {
|
|||||||
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
|
||||||
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
|
||||||
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
|
||||||
|
LIST_CERTIFICATE_TEMPLATES = "list-certificate-templates",
|
||||||
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
|
||||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
|
||||||
CREATE_CERTIFICATE_TEMPLATE_V2 = "create-certificate-template-v2",
|
|
||||||
UPDATE_CERTIFICATE_TEMPLATE_V2 = "update-certificate-template-v2",
|
|
||||||
DELETE_CERTIFICATE_TEMPLATE_V2 = "delete-certificate-template-v2",
|
|
||||||
GET_CERTIFICATE_TEMPLATE_V2 = "get-certificate-template-v2",
|
|
||||||
LIST_CERTIFICATE_TEMPLATES_V2 = "list-certificate-templates-v2",
|
|
||||||
CREATE_CERTIFICATE_PROFILE = "create-certificate-profile",
|
CREATE_CERTIFICATE_PROFILE = "create-certificate-profile",
|
||||||
UPDATE_CERTIFICATE_PROFILE = "update-certificate-profile",
|
UPDATE_CERTIFICATE_PROFILE = "update-certificate-profile",
|
||||||
DELETE_CERTIFICATE_PROFILE = "delete-certificate-profile",
|
DELETE_CERTIFICATE_PROFILE = "delete-certificate-profile",
|
||||||
@@ -2525,46 +2521,6 @@ interface LoadProjectKmsBackupEvent {
|
|||||||
metadata: Record<string, string>; // no metadata yet
|
metadata: Record<string, string>; // no metadata yet
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateCertificateTemplate {
|
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
|
||||||
metadata: {
|
|
||||||
certificateTemplateId: string;
|
|
||||||
caId: string;
|
|
||||||
pkiCollectionId?: string;
|
|
||||||
name: string;
|
|
||||||
commonName: string;
|
|
||||||
subjectAlternativeName: string;
|
|
||||||
ttl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetCertificateTemplate {
|
|
||||||
type: EventType.GET_CERTIFICATE_TEMPLATE;
|
|
||||||
metadata: {
|
|
||||||
certificateTemplateId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateCertificateTemplate {
|
|
||||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
|
||||||
metadata: {
|
|
||||||
certificateTemplateId: string;
|
|
||||||
caId: string;
|
|
||||||
pkiCollectionId?: string;
|
|
||||||
name: string;
|
|
||||||
commonName: string;
|
|
||||||
subjectAlternativeName: string;
|
|
||||||
ttl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteCertificateTemplate {
|
|
||||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
|
|
||||||
metadata: {
|
|
||||||
certificateTemplateId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OrgAdminAccessProjectEvent {
|
interface OrgAdminAccessProjectEvent {
|
||||||
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
type: EventType.ORG_ADMIN_ACCESS_PROJECT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2611,8 +2567,8 @@ interface GetCertificateTemplateEstConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreateCertificateTemplateV2 {
|
interface CreateCertificateTemplate {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_V2;
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -2620,30 +2576,32 @@ interface CreateCertificateTemplateV2 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateCertificateTemplateV2 {
|
interface UpdateCertificateTemplate {
|
||||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_V2;
|
type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteCertificateTemplateV2 {
|
interface DeleteCertificateTemplate {
|
||||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE_V2;
|
type: EventType.DELETE_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetCertificateTemplateV2 {
|
interface GetCertificateTemplate {
|
||||||
type: EventType.GET_CERTIFICATE_TEMPLATE_V2;
|
type: EventType.GET_CERTIFICATE_TEMPLATE;
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
|
name: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListCertificateTemplatesV2 {
|
interface ListCertificateTemplates {
|
||||||
type: EventType.LIST_CERTIFICATE_TEMPLATES_V2;
|
type: EventType.LIST_CERTIFICATE_TEMPLATES;
|
||||||
metadata: {
|
metadata: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
@@ -2710,7 +2668,7 @@ interface OrderCertificateFromProfile {
|
|||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: string;
|
certificateProfileId: string;
|
||||||
orderId: string;
|
orderId: string;
|
||||||
identifiers: string[];
|
subjectAlternativeNames: string[];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4167,18 +4125,14 @@ export type Event =
|
|||||||
| LoadProjectKmsBackupEvent
|
| LoadProjectKmsBackupEvent
|
||||||
| OrgAdminAccessProjectEvent
|
| OrgAdminAccessProjectEvent
|
||||||
| OrgAdminBypassSSOEvent
|
| OrgAdminBypassSSOEvent
|
||||||
| CreateCertificateTemplate
|
|
||||||
| UpdateCertificateTemplate
|
|
||||||
| GetCertificateTemplate
|
|
||||||
| DeleteCertificateTemplate
|
|
||||||
| CreateCertificateTemplateEstConfig
|
| CreateCertificateTemplateEstConfig
|
||||||
| UpdateCertificateTemplateEstConfig
|
| UpdateCertificateTemplateEstConfig
|
||||||
| GetCertificateTemplateEstConfig
|
| GetCertificateTemplateEstConfig
|
||||||
| CreateCertificateTemplateV2
|
| CreateCertificateTemplate
|
||||||
| UpdateCertificateTemplateV2
|
| UpdateCertificateTemplate
|
||||||
| DeleteCertificateTemplateV2
|
| DeleteCertificateTemplate
|
||||||
| GetCertificateTemplateV2
|
| GetCertificateTemplate
|
||||||
| ListCertificateTemplatesV2
|
| ListCertificateTemplates
|
||||||
| CreateCertificateProfile
|
| CreateCertificateProfile
|
||||||
| UpdateCertificateProfile
|
| UpdateCertificateProfile
|
||||||
| DeleteCertificateProfile
|
| DeleteCertificateProfile
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ const buildMemberPermissionRules = () => {
|
|||||||
|
|
||||||
// double check if all CRUD are needed for CA and Certificates
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
|
||||||
|
can([ProjectPermissionPkiTemplateActions.Read], ProjectPermissionSub.CertificateTemplates);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { AuthMode } from "@app/services/auth/auth-type";
|
|||||||
import {
|
import {
|
||||||
createCertificateProfileSchema,
|
createCertificateProfileSchema,
|
||||||
deleteCertificateProfileSchema,
|
deleteCertificateProfileSchema,
|
||||||
getCertificateProfileByIdSchema,
|
|
||||||
listCertificateProfilesSchema,
|
listCertificateProfilesSchema,
|
||||||
updateCertificateProfileSchema
|
updateCertificateProfileSchema
|
||||||
} from "@app/services/certificate-profile/certificate-profile-schemas";
|
} from "@app/services/certificate-profile/certificate-profile-schemas";
|
||||||
@@ -49,7 +48,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
type: EventType.CREATE_CERTIFICATE_PROFILE,
|
type: EventType.CREATE_CERTIFICATE_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: certificateProfile.id,
|
certificateProfileId: certificateProfile.id,
|
||||||
name: certificateProfile.name,
|
name: certificateProfile.slug,
|
||||||
projectId: certificateProfile.projectId,
|
projectId: certificateProfile.projectId,
|
||||||
enrollmentType: certificateProfile.enrollmentType
|
enrollmentType: certificateProfile.enrollmentType
|
||||||
}
|
}
|
||||||
@@ -125,7 +124,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
params: getCertificateProfileByIdSchema,
|
params: z.object({
|
||||||
|
id: z.string().min(1)
|
||||||
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
includeMetrics: z.coerce.boolean().optional().default(false),
|
includeMetrics: z.coerce.boolean().optional().default(false),
|
||||||
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
expiringDays: z.coerce.number().min(1).max(365).optional().default(7)
|
||||||
@@ -262,7 +263,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
params: getCertificateProfileByIdSchema,
|
params: z.object({
|
||||||
|
id: z.string().min(1)
|
||||||
|
}),
|
||||||
body: updateCertificateProfileSchema,
|
body: updateCertificateProfileSchema,
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -288,7 +291,7 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
type: EventType.UPDATE_CERTIFICATE_PROFILE,
|
type: EventType.UPDATE_CERTIFICATE_PROFILE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: certificateProfile.id,
|
certificateProfileId: certificateProfile.id,
|
||||||
name: certificateProfile.name
|
name: certificateProfile.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -347,7 +350,9 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
|||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiCertificateProfiles],
|
tags: [ApiDocsTags.PkiCertificateProfiles],
|
||||||
params: getCertificateProfileByIdSchema,
|
params: z.object({
|
||||||
|
id: z.string().min(1)
|
||||||
|
}),
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
offset: z.number().min(0).default(0),
|
offset: z.number().min(0).default(0),
|
||||||
limit: z.number().min(1).max(100).default(20),
|
limit: z.number().min(1).max(100).default(20),
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERTIFICATE_TEMPLATE,
|
type: EventType.GET_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
name: certificateTemplate.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -116,12 +117,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
caId: certificateTemplate.caId,
|
|
||||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
|
||||||
name: certificateTemplate.name,
|
name: certificateTemplate.name,
|
||||||
commonName: certificateTemplate.commonName,
|
projectId: certificateTemplate.projectId
|
||||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
|
||||||
ttl: certificateTemplate.ttl
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -184,12 +181,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
caId: certificateTemplate.caId,
|
name: certificateTemplate.name
|
||||||
pkiCollectionId: certificateTemplate.pkiCollectionId as string,
|
|
||||||
name: certificateTemplate.name,
|
|
||||||
commonName: certificateTemplate.commonName,
|
|
||||||
subjectAlternativeName: certificateTemplate.subjectAlternativeName,
|
|
||||||
ttl: certificateTemplate.ttl
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -230,7 +222,8 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
|
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
name: certificateTemplate.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId,
|
projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.CREATE_CERTIFICATE_TEMPLATE_V2,
|
type: EventType.CREATE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
name: certificateTemplate.name,
|
name: certificateTemplate.slug,
|
||||||
projectId: certificateTemplate.projectId
|
projectId: certificateTemplate.projectId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,7 +92,7 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: req.query.projectId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.LIST_CERTIFICATE_TEMPLATES_V2,
|
type: EventType.LIST_CERTIFICATE_TEMPLATES,
|
||||||
metadata: {
|
metadata: {
|
||||||
projectId: req.query.projectId
|
projectId: req.query.projectId
|
||||||
}
|
}
|
||||||
@@ -133,9 +133,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateTemplate.projectId,
|
projectId: certificateTemplate.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_CERTIFICATE_TEMPLATE_V2,
|
type: EventType.GET_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
name: certificateTemplate.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -176,10 +177,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateTemplate.projectId,
|
projectId: certificateTemplate.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_V2,
|
type: EventType.UPDATE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id,
|
certificateTemplateId: certificateTemplate.id,
|
||||||
name: certificateTemplate.name
|
name: certificateTemplate.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -218,9 +219,10 @@ export const registerCertificateTemplatesV2Router = async (server: FastifyZodPro
|
|||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId: certificateTemplate.projectId,
|
projectId: certificateTemplate.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.DELETE_CERTIFICATE_TEMPLATE_V2,
|
type: EventType.DELETE_CERTIFICATE_TEMPLATE,
|
||||||
metadata: {
|
metadata: {
|
||||||
certificateTemplateId: certificateTemplate.id
|
certificateTemplateId: certificateTemplate.id,
|
||||||
|
name: certificateTemplate.slug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
tags: [ApiDocsTags.PkiCertificates],
|
tags: [ApiDocsTags.PkiCertificates],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
profileId: z.string().uuid(),
|
profileId: z.string().uuid(),
|
||||||
identifiers: z
|
subjectAlternativeNames: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["dns", "ip"]),
|
type: z.enum(["dns", "ip"]),
|
||||||
@@ -217,7 +217,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
orderId: z.string(),
|
orderId: z.string(),
|
||||||
status: z.enum(["pending", "processing", "valid", "invalid"]),
|
status: z.enum(["pending", "processing", "valid", "invalid"]),
|
||||||
identifiers: z.array(
|
subjectAlternativeNames: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["dns", "ip"]),
|
type: z.enum(["dns", "ip"]),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
@@ -256,7 +256,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
profileId: req.body.profileId,
|
profileId: req.body.profileId,
|
||||||
certificateOrder: {
|
certificateOrder: {
|
||||||
identifiers: req.body.identifiers,
|
subjectAlternativeNames: req.body.subjectAlternativeNames,
|
||||||
validity: {
|
validity: {
|
||||||
ttl: req.body.ttl
|
ttl: req.body.ttl
|
||||||
},
|
},
|
||||||
@@ -286,7 +286,7 @@ export const registerCertificatesRouter = async (server: FastifyZodProvider) =>
|
|||||||
metadata: {
|
metadata: {
|
||||||
certificateProfileId: req.body.profileId,
|
certificateProfileId: req.body.profileId,
|
||||||
orderId: data.orderId,
|
orderId: data.orderId,
|
||||||
identifiers: req.body.identifiers.map((id) => `${id.type}:${id.value}`)
|
subjectAlternativeNames: req.body.subjectAlternativeNames.map((san) => `${san.type}:${san.value}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,18 +68,24 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
(tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
|
(tx || db).ref("name").withSchema(TableName.CertificateAuthority).as("caName"),
|
||||||
(tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"),
|
(tx || db).ref("id").withSchema(TableName.CertificateTemplateV2).as("templateId"),
|
||||||
(tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"),
|
(tx || db).ref("projectId").withSchema(TableName.CertificateTemplateV2).as("templateProjectId"),
|
||||||
(tx || db).ref("name").withSchema(TableName.CertificateTemplateV2).as("templateName"),
|
(tx || db).ref("slug").withSchema(TableName.CertificateTemplateV2).as("templateName"),
|
||||||
(tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"),
|
(tx || db).ref("description").withSchema(TableName.CertificateTemplateV2).as("templateDescription"),
|
||||||
(tx || db).ref("id").withSchema(TableName.EstEnrollmentConfig).as("estConfigId"),
|
(tx || db).ref("id").withSchema(TableName.PkiEstEnrollmentConfig).as("estConfigId"),
|
||||||
(tx || db)
|
(tx || db)
|
||||||
.ref("disableBootstrapCaValidation")
|
.ref("disableBootstrapCaValidation")
|
||||||
.withSchema(TableName.EstEnrollmentConfig)
|
.withSchema(TableName.PkiEstEnrollmentConfig)
|
||||||
.as("estConfigDisableBootstrapCaValidation"),
|
.as("estConfigDisableBootstrapCaValidation"),
|
||||||
(tx || db).ref("hashedPassphrase").withSchema(TableName.EstEnrollmentConfig).as("estConfigHashedPassphrase"),
|
(tx || db)
|
||||||
(tx || db).ref("encryptedCaChain").withSchema(TableName.EstEnrollmentConfig).as("estConfigEncryptedCaChain"),
|
.ref("hashedPassphrase")
|
||||||
(tx || db).ref("id").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigId"),
|
.withSchema(TableName.PkiEstEnrollmentConfig)
|
||||||
(tx || db).ref("autoRenew").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigAutoRenew"),
|
.as("estConfigHashedPassphrase"),
|
||||||
(tx || db).ref("autoRenewDays").withSchema(TableName.ApiEnrollmentConfig).as("apiConfigAutoRenewDays")
|
(tx || db)
|
||||||
|
.ref("encryptedCaChain")
|
||||||
|
.withSchema(TableName.PkiEstEnrollmentConfig)
|
||||||
|
.as("estConfigEncryptedCaChain"),
|
||||||
|
(tx || db).ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigId"),
|
||||||
|
(tx || db).ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
|
||||||
|
(tx || db).ref("autoRenewDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenewDays")
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.CertificateAuthority,
|
TableName.CertificateAuthority,
|
||||||
@@ -92,14 +98,14 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.CertificateTemplateV2}.id`
|
`${TableName.CertificateTemplateV2}.id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.EstEnrollmentConfig,
|
TableName.PkiEstEnrollmentConfig,
|
||||||
`${TableName.CertificateProfile}.estConfigId`,
|
`${TableName.CertificateProfile}.estConfigId`,
|
||||||
`${TableName.EstEnrollmentConfig}.id`
|
`${TableName.PkiEstEnrollmentConfig}.id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.ApiEnrollmentConfig,
|
TableName.PkiApiEnrollmentConfig,
|
||||||
`${TableName.CertificateProfile}.apiConfigId`,
|
`${TableName.CertificateProfile}.apiConfigId`,
|
||||||
`${TableName.ApiEnrollmentConfig}.id`
|
`${TableName.PkiApiEnrollmentConfig}.id`
|
||||||
)
|
)
|
||||||
.where(`${TableName.CertificateProfile}.id`, id)
|
.where(`${TableName.CertificateProfile}.id`, id)
|
||||||
.first();
|
.first();
|
||||||
@@ -151,9 +157,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
if (search) {
|
if (search) {
|
||||||
query = query.where((builder) => {
|
query = query.where((builder) => {
|
||||||
void builder
|
void builder
|
||||||
.whereILike(`${TableName.CertificateProfile}.name`, `%${search}%`)
|
.whereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`)
|
||||||
.orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`)
|
.orWhereILike(`${TableName.CertificateProfile}.description`, `%${search}%`);
|
||||||
.orWhereILike(`${TableName.CertificateProfile}.slug`, `%${search}%`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,10 +230,7 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query = query.where((builder) => {
|
query = query.where((builder) => {
|
||||||
void builder
|
void builder.orWhereILike("description", `%${search}%`).orWhereILike("slug", `%${search}%`);
|
||||||
.whereILike("name", `%${search}%`)
|
|
||||||
.orWhereILike("description", `%${search}%`)
|
|
||||||
.orWhereILike("slug", `%${search}%`);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +251,9 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
|
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const certificateProfile = await (tx || db)(TableName.CertificateProfile).where({ name, projectId }).first();
|
const certificateProfile = await (tx || db)(TableName.CertificateProfile)
|
||||||
|
.where({ slug: name, projectId })
|
||||||
|
.first();
|
||||||
return certificateProfile;
|
return certificateProfile;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find certificate profile by name and project id" });
|
throw new DatabaseError({ error, name: "Find certificate profile by name and project id" });
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ export const createCertificateProfileSchema = z
|
|||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
caId: z.string().uuid(),
|
caId: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid(),
|
certificateTemplateId: z.string().uuid(),
|
||||||
name: z.string().min(1).max(255),
|
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
@@ -46,7 +45,6 @@ export const createCertificateProfileSchema = z
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const updateCertificateProfileSchema = z.object({
|
export const updateCertificateProfileSchema = z.object({
|
||||||
name: z.string().min(1).max(255).optional(),
|
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ describe("CertificateProfileService", () => {
|
|||||||
const sampleProfile: TCertificateProfile = {
|
const sampleProfile: TCertificateProfile = {
|
||||||
id: "profile-123",
|
id: "profile-123",
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
name: "Test Profile",
|
|
||||||
description: "Test certificate profile",
|
description: "Test certificate profile",
|
||||||
slug: "test-profile",
|
slug: "test-profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
@@ -174,9 +173,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
describe("createProfile", () => {
|
describe("createProfile", () => {
|
||||||
const validProfileData = {
|
const validProfileData = {
|
||||||
name: "New Profile",
|
|
||||||
description: "New test profile",
|
|
||||||
slug: "new-profile",
|
slug: "new-profile",
|
||||||
|
description: "New test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -204,9 +202,8 @@ describe("CertificateProfileService", () => {
|
|||||||
expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123");
|
expect(mockCertificateTemplateV2DAL.findById).toHaveBeenCalledWith("template-123");
|
||||||
expect(mockCertificateProfileDAL.findBySlugAndProjectId).toHaveBeenCalledWith("new-profile", "project-123");
|
expect(mockCertificateProfileDAL.findBySlugAndProjectId).toHaveBeenCalledWith("new-profile", "project-123");
|
||||||
expect(mockCertificateProfileDAL.create).toHaveBeenCalledWith({
|
expect(mockCertificateProfileDAL.create).toHaveBeenCalledWith({
|
||||||
name: "New Profile",
|
|
||||||
description: "New test profile",
|
|
||||||
slug: "new-profile",
|
slug: "new-profile",
|
||||||
|
description: "New test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -273,9 +270,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
it("should throw ForbiddenRequestError for API enrollment without API config", async () => {
|
it("should throw ForbiddenRequestError for API enrollment without API config", async () => {
|
||||||
const invalidData = {
|
const invalidData = {
|
||||||
name: "Invalid Profile",
|
|
||||||
description: "Invalid test profile",
|
|
||||||
slug: "invalid-profile",
|
slug: "invalid-profile",
|
||||||
|
description: "Invalid test profile",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123"
|
certificateTemplateId: "template-123"
|
||||||
@@ -292,9 +288,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
it("should create profile with API enrollment", async () => {
|
it("should create profile with API enrollment", async () => {
|
||||||
const apiProfileData = {
|
const apiProfileData = {
|
||||||
name: "API Profile",
|
|
||||||
description: "Profile with API enrollment",
|
|
||||||
slug: "api-profile",
|
slug: "api-profile",
|
||||||
|
description: "Profile with API enrollment",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -317,7 +312,7 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
describe("updateProfile", () => {
|
describe("updateProfile", () => {
|
||||||
const updateData = {
|
const updateData = {
|
||||||
name: "Updated Profile",
|
slug: "updated-profile",
|
||||||
description: "Updated description"
|
description: "Updated description"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -333,7 +328,7 @@ describe("CertificateProfileService", () => {
|
|||||||
data: updateData
|
data: updateData
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.name).toBe("Updated Profile");
|
expect(result.slug).toBe("updated-profile");
|
||||||
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
expect(mockCertificateProfileDAL.findById).toHaveBeenCalledWith("profile-123");
|
||||||
expect(mockCertificateProfileDAL.updateById).toHaveBeenCalledWith("profile-123", updateData);
|
expect(mockCertificateProfileDAL.updateById).toHaveBeenCalledWith("profile-123", updateData);
|
||||||
});
|
});
|
||||||
@@ -697,9 +692,8 @@ describe("CertificateProfileService", () => {
|
|||||||
describe("profile configuration validation", () => {
|
describe("profile configuration validation", () => {
|
||||||
it("should validate EST enrollment configuration", async () => {
|
it("should validate EST enrollment configuration", async () => {
|
||||||
const estProfileData = {
|
const estProfileData = {
|
||||||
name: "EST Profile",
|
|
||||||
description: "Profile with EST enrollment",
|
|
||||||
slug: "est-profile",
|
slug: "est-profile",
|
||||||
|
description: "Profile with EST enrollment",
|
||||||
enrollmentType: EnrollmentType.EST,
|
enrollmentType: EnrollmentType.EST,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -737,9 +731,8 @@ describe("CertificateProfileService", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
const duplicateSlugData = {
|
const duplicateSlugData = {
|
||||||
name: "Different Profile Name",
|
slug: "different-profile-name",
|
||||||
description: "Profile with duplicate slug",
|
description: "Profile with duplicate slug",
|
||||||
slug: "test-profile",
|
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -763,9 +756,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
it("should validate auto-renewal configuration", async () => {
|
it("should validate auto-renewal configuration", async () => {
|
||||||
const autoRenewData = {
|
const autoRenewData = {
|
||||||
name: "Auto Renew Profile",
|
slug: "auto-renew-profile",
|
||||||
description: "Profile with auto-renewal",
|
description: "Profile with auto-renewal",
|
||||||
slug: "auto-renew",
|
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -987,9 +979,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
it("should handle invalid template reference during profile creation", async () => {
|
it("should handle invalid template reference during profile creation", async () => {
|
||||||
const profileData = {
|
const profileData = {
|
||||||
name: "Invalid Template Profile",
|
slug: "invalid-template-profile",
|
||||||
description: "Profile with invalid template",
|
description: "Profile with invalid template",
|
||||||
slug: "invalid-template",
|
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "nonexistent-template",
|
certificateTemplateId: "nonexistent-template",
|
||||||
@@ -1013,9 +1004,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
it("should handle concurrent profile creation conflicts", async () => {
|
it("should handle concurrent profile creation conflicts", async () => {
|
||||||
const conflictingData = {
|
const conflictingData = {
|
||||||
name: "Concurrent Profile",
|
|
||||||
description: "Profile created concurrently",
|
|
||||||
slug: "concurrent-profile",
|
slug: "concurrent-profile",
|
||||||
|
description: "Profile created concurrently",
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
@@ -1042,9 +1032,8 @@ describe("CertificateProfileService", () => {
|
|||||||
describe("permission and security", () => {
|
describe("permission and security", () => {
|
||||||
it("should validate project ownership for cross-project template access", async () => {
|
it("should validate project ownership for cross-project template access", async () => {
|
||||||
const crossProjectData = {
|
const crossProjectData = {
|
||||||
name: "Cross Project Profile",
|
slug: "cross-project-profile",
|
||||||
description: "Profile using template from different project",
|
description: "Profile using template from different project",
|
||||||
slug: "cross-project",
|
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-456",
|
certificateTemplateId: "template-456",
|
||||||
@@ -1056,7 +1045,7 @@ describe("CertificateProfileService", () => {
|
|||||||
const foreignTemplate = {
|
const foreignTemplate = {
|
||||||
id: "template-456",
|
id: "template-456",
|
||||||
projectId: "different-project-456",
|
projectId: "different-project-456",
|
||||||
name: "Foreign Template"
|
slug: "foreign-template"
|
||||||
};
|
};
|
||||||
|
|
||||||
(mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(foreignTemplate);
|
(mockCertificateTemplateV2DAL.findById as any).mockResolvedValue(foreignTemplate);
|
||||||
@@ -1072,9 +1061,8 @@ describe("CertificateProfileService", () => {
|
|||||||
|
|
||||||
it("should validate slug format constraints", async () => {
|
it("should validate slug format constraints", async () => {
|
||||||
const invalidSlugData = {
|
const invalidSlugData = {
|
||||||
name: "Invalid Slug Profile",
|
slug: "invalid-slug-profile",
|
||||||
description: "Profile with invalid slug format",
|
description: "Profile with invalid slug format",
|
||||||
slug: "Invalid_Slug_With_Underscores_And_Caps",
|
|
||||||
enrollmentType: EnrollmentType.API,
|
enrollmentType: EnrollmentType.API,
|
||||||
caId: "ca-123",
|
caId: "ca-123",
|
||||||
certificateTemplateId: "template-123",
|
certificateTemplateId: "template-123",
|
||||||
|
|||||||
@@ -277,7 +277,28 @@ export const certificateProfileServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedProfile = await certificateProfileDAL.updateById(profileId, data);
|
const { estConfig, apiConfig, ...profileUpdateData } = data;
|
||||||
|
|
||||||
|
if (estConfig && existingProfile.estConfigId) {
|
||||||
|
await estEnrollmentConfigDAL.updateById(existingProfile.estConfigId, {
|
||||||
|
disableBootstrapCaValidation: estConfig.disableBootstrapCaValidation,
|
||||||
|
...(estConfig.passphrase && {
|
||||||
|
hashedPassphrase: await crypto.hashing().createHash(estConfig.passphrase, getConfig().SALT_ROUNDS)
|
||||||
|
}),
|
||||||
|
...(estConfig.caChain && {
|
||||||
|
encryptedCaChain: Buffer.from(estConfig.caChain, "base64")
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiConfig && existingProfile.apiConfigId) {
|
||||||
|
await apiEnrollmentConfigDAL.updateById(existingProfile.apiConfigId, {
|
||||||
|
autoRenew: apiConfig.autoRenew,
|
||||||
|
autoRenewDays: apiConfig.autoRenewDays
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedProfile = await certificateProfileDAL.updateById(profileId, profileUpdateData);
|
||||||
return convertDalToService(updatedProfile);
|
return convertDalToService(updatedProfile);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ export type TCertificateProfileInsert = Omit<TCertificateProfilesInsert, "enroll
|
|||||||
|
|
||||||
export type TCertificateProfileUpdate = Omit<TCertificateProfilesUpdate, "enrollmentType"> & {
|
export type TCertificateProfileUpdate = Omit<TCertificateProfilesUpdate, "enrollmentType"> & {
|
||||||
enrollmentType?: EnrollmentType;
|
enrollmentType?: EnrollmentType;
|
||||||
|
estConfig?: {
|
||||||
|
disableBootstrapCaValidation?: boolean;
|
||||||
|
passphrase?: string;
|
||||||
|
caChain?: string;
|
||||||
|
};
|
||||||
|
apiConfig?: {
|
||||||
|
autoRenew?: boolean;
|
||||||
|
autoRenewDays?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query = query.where((builder) => {
|
query = query.where((builder) => {
|
||||||
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
|
void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
query = query.where((builder) => {
|
query = query.where((builder) => {
|
||||||
void builder.whereILike("name", `%${search}%`).orWhereILike("description", `%${search}%`);
|
void builder.whereILike("slug", `%${search}%`).orWhereILike("description", `%${search}%`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,10 +176,10 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByNameAndProjectId = async (name: string, projectId: string, tx?: Knex) => {
|
const findBySlugAndProjectId = async (slug: string, projectId: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2)
|
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2)
|
||||||
.where({ name, projectId })
|
.where({ slug, projectId })
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
if (!certificateTemplateV2) {
|
if (!certificateTemplateV2) {
|
||||||
@@ -188,7 +188,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
return parseJsonFields(certificateTemplateV2);
|
return parseJsonFields(certificateTemplateV2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find certificate template v2 by name and project id" });
|
throw new DatabaseError({ error, name: "Find certificate template v2 by slug and project id" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
|
|||||||
findById,
|
findById,
|
||||||
findByProjectId,
|
findByProjectId,
|
||||||
countByProjectId,
|
countByProjectId,
|
||||||
findByNameAndProjectId,
|
findBySlugAndProjectId,
|
||||||
isTemplateInUse
|
isTemplateInUse
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const attributeTypeSchema = z.enum(["common_name"]);
|
const attributeTypeSchema = z.enum(["common_name"]);
|
||||||
@@ -87,7 +88,11 @@ export const templateV2KeyAlgorithmSchema = z.object({
|
|||||||
|
|
||||||
export const createCertificateTemplateV2Schema = z.object({
|
export const createCertificateTemplateV2Schema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
name: z.string().min(1).max(255),
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(255)
|
||||||
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens"),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
attributes: z.array(templateV2AttributeSchema).optional(),
|
attributes: z.array(templateV2AttributeSchema).optional(),
|
||||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||||
@@ -99,7 +104,12 @@ export const createCertificateTemplateV2Schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const updateCertificateTemplateV2Schema = z.object({
|
export const updateCertificateTemplateV2Schema = z.object({
|
||||||
name: z.string().min(1).max(255).optional(),
|
slug: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(255)
|
||||||
|
.regex(new RE2("^[a-z0-9-]+$"), "Slug must contain only lowercase letters, numbers, and hyphens")
|
||||||
|
.optional(),
|
||||||
description: z.string().max(1000).optional(),
|
description: z.string().max(1000).optional(),
|
||||||
attributes: z.array(templateV2AttributeSchema).optional(),
|
attributes: z.array(templateV2AttributeSchema).optional(),
|
||||||
keyUsages: templateV2KeyUsagesSchema.optional(),
|
keyUsages: templateV2KeyUsagesSchema.optional(),
|
||||||
@@ -114,6 +124,11 @@ export const getCertificateTemplateV2ByIdSchema = z.object({
|
|||||||
id: z.string().uuid()
|
id: z.string().uuid()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const getCertificateTemplateV2BySlugSchema = z.object({
|
||||||
|
projectId: z.string().min(1),
|
||||||
|
slug: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
export const listCertificateTemplatesV2Schema = z.object({
|
export const listCertificateTemplatesV2Schema = z.object({
|
||||||
projectId: z.string().min(1),
|
projectId: z.string().min(1),
|
||||||
offset: z.coerce.number().min(0).default(0),
|
offset: z.coerce.number().min(0).default(0),
|
||||||
|
|||||||
+6
-4
@@ -27,6 +27,7 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
let service: TCertificateTemplateV2ServiceFactory;
|
let service: TCertificateTemplateV2ServiceFactory;
|
||||||
|
|
||||||
const mockCertificateTemplateV2DAL = {
|
const mockCertificateTemplateV2DAL = {
|
||||||
|
findBySlugAndProjectId: vi.fn(),
|
||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
findById: vi.fn(),
|
findById: vi.fn(),
|
||||||
updateById: vi.fn(),
|
updateById: vi.fn(),
|
||||||
@@ -99,7 +100,7 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
const sampleTemplate: TCertificateTemplateV2 = {
|
const sampleTemplate: TCertificateTemplateV2 = {
|
||||||
id: "template-123",
|
id: "template-123",
|
||||||
projectId: "project-123",
|
projectId: "project-123",
|
||||||
name: "Web Server Template",
|
slug: "web-server-template",
|
||||||
description: "Template for web server certificates",
|
description: "Template for web server certificates",
|
||||||
...samplePolicy,
|
...samplePolicy,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@@ -136,6 +137,7 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mockCertificateTemplateV2DAL.findByNameAndProjectId.mockResolvedValue(null);
|
mockCertificateTemplateV2DAL.findByNameAndProjectId.mockResolvedValue(null);
|
||||||
|
mockCertificateTemplateV2DAL.findBySlugAndProjectId.mockResolvedValue(null);
|
||||||
|
|
||||||
service = certificateTemplateV2ServiceFactory({
|
service = certificateTemplateV2ServiceFactory({
|
||||||
certificateTemplateV2DAL: mockCertificateTemplateV2DAL as TCertificateTemplateV2DALFactory,
|
certificateTemplateV2DAL: mockCertificateTemplateV2DAL as TCertificateTemplateV2DALFactory,
|
||||||
@@ -149,7 +151,7 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
|
|
||||||
describe("createTemplateV2", () => {
|
describe("createTemplateV2", () => {
|
||||||
const createData: Omit<TCertificateTemplateV2Insert, "projectId"> = {
|
const createData: Omit<TCertificateTemplateV2Insert, "projectId"> = {
|
||||||
name: "Test Template",
|
slug: "test-template",
|
||||||
description: "Test description",
|
description: "Test description",
|
||||||
...samplePolicy
|
...samplePolicy
|
||||||
};
|
};
|
||||||
@@ -239,7 +241,7 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
|
|
||||||
describe("updateTemplateV2", () => {
|
describe("updateTemplateV2", () => {
|
||||||
it("should update template with valid data", async () => {
|
it("should update template with valid data", async () => {
|
||||||
const updateData = { name: "Updated Template Name" };
|
const updateData = { slug: "updated-template-name" };
|
||||||
const updatedTemplate = { ...sampleTemplate, ...updateData };
|
const updatedTemplate = { ...sampleTemplate, ...updateData };
|
||||||
|
|
||||||
mockCertificateTemplateV2DAL.findById.mockResolvedValue(sampleTemplate);
|
mockCertificateTemplateV2DAL.findById.mockResolvedValue(sampleTemplate);
|
||||||
@@ -263,7 +265,7 @@ describe("CertificateTemplateV2Service", () => {
|
|||||||
service.updateTemplateV2({
|
service.updateTemplateV2({
|
||||||
...mockActor,
|
...mockActor,
|
||||||
templateId: "nonexistent-template",
|
templateId: "nonexistent-template",
|
||||||
data: { name: "Updated Name" }
|
data: { slug: "updated-name" }
|
||||||
})
|
})
|
||||||
).rejects.toThrow(NotFoundError);
|
).rejects.toThrow(NotFoundError);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { ActionProjectType } from "@app/db/schemas";
|
import { ActionProjectType } from "@app/db/schemas";
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
|
import { TCertificateTemplateV2DALFactory } from "./certificate-template-v2-dal";
|
||||||
@@ -115,6 +117,28 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateTemplateSlug = (baseSlug?: string): string => {
|
||||||
|
if (baseSlug) {
|
||||||
|
return slugify(baseSlug);
|
||||||
|
}
|
||||||
|
return slugify(alphaNumericNanoId(12));
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureUniqueSlug = async (projectId: string, desiredSlug: string, templateId?: string): Promise<string> => {
|
||||||
|
const existingTemplate = await certificateTemplateV2DAL.findBySlugAndProjectId(desiredSlug, projectId);
|
||||||
|
if (!existingTemplate || (templateId && existingTemplate.id === templateId)) {
|
||||||
|
return desiredSlug;
|
||||||
|
}
|
||||||
|
const alternativeSlug = `${desiredSlug}-${alphaNumericNanoId(8)}`;
|
||||||
|
const existingAlternative = await certificateTemplateV2DAL.findBySlugAndProjectId(alternativeSlug, projectId);
|
||||||
|
if (!existingAlternative) {
|
||||||
|
return alternativeSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomSlug = slugify(alphaNumericNanoId(12));
|
||||||
|
return randomSlug;
|
||||||
|
};
|
||||||
|
|
||||||
const validateRequestAgainstPolicy = (
|
const validateRequestAgainstPolicy = (
|
||||||
template: TCertificateTemplateV2,
|
template: TCertificateTemplateV2,
|
||||||
request: TCertificateRequest
|
request: TCertificateRequest
|
||||||
@@ -361,8 +385,12 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
keyAlgorithm: data.keyAlgorithm
|
keyAlgorithm: data.keyAlgorithm
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slug = data.slug || generateTemplateSlug();
|
||||||
|
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
|
||||||
|
|
||||||
const template = await certificateTemplateV2DAL.create({
|
const template = await certificateTemplateV2DAL.create({
|
||||||
...data,
|
...data,
|
||||||
|
slug: uniqueSlug,
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -417,7 +445,13 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
validateTemplatePolicy(mergedPolicy);
|
validateTemplatePolicy(mergedPolicy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, data);
|
const updateData = { ...data };
|
||||||
|
if (data.slug && typeof data.slug === "string" && data.slug !== existingTemplate.slug) {
|
||||||
|
const uniqueSlug = await ensureUniqueSlug(existingTemplate.projectId, data.slug, templateId);
|
||||||
|
updateData.slug = uniqueSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTemplate = await certificateTemplateV2DAL.updateById(templateId, updateData);
|
||||||
if (!updatedTemplate) {
|
if (!updatedTemplate) {
|
||||||
throw new NotFoundError({ message: "Failed to update certificate template" });
|
throw new NotFoundError({ message: "Failed to update certificate template" });
|
||||||
}
|
}
|
||||||
@@ -459,6 +493,43 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
return template;
|
return template;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTemplateV2BySlug = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
projectId,
|
||||||
|
slug
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string;
|
||||||
|
projectId: string;
|
||||||
|
slug: string;
|
||||||
|
}): Promise<TCertificateTemplateV2> => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.CertificateManager
|
||||||
|
});
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionPkiTemplateActions.Read,
|
||||||
|
ProjectPermissionSub.CertificateTemplates
|
||||||
|
);
|
||||||
|
|
||||||
|
const template = await certificateTemplateV2DAL.findBySlugAndProjectId(slug, projectId);
|
||||||
|
if (!template) {
|
||||||
|
throw new NotFoundError({ message: "Certificate template not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return template;
|
||||||
|
};
|
||||||
|
|
||||||
const listTemplatesV2 = async ({
|
const listTemplatesV2 = async ({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -571,6 +642,7 @@ export const certificateTemplateV2ServiceFactory = ({
|
|||||||
createTemplateV2,
|
createTemplateV2,
|
||||||
updateTemplateV2,
|
updateTemplateV2,
|
||||||
getTemplateV2ById,
|
getTemplateV2ById,
|
||||||
|
getTemplateV2BySlug,
|
||||||
listTemplatesV2,
|
listTemplatesV2,
|
||||||
deleteTemplateV2,
|
deleteTemplateV2,
|
||||||
validateCertificateRequest
|
validateCertificateRequest
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export type TCertificateTemplateV2Insert = Omit<
|
|||||||
export type TCertificateTemplateV2Update = Partial<
|
export type TCertificateTemplateV2Update = Partial<
|
||||||
Pick<
|
Pick<
|
||||||
TCertificateTemplateV2,
|
TCertificateTemplateV2,
|
||||||
| "name"
|
| "slug"
|
||||||
| "description"
|
| "description"
|
||||||
| "attributes"
|
| "attributes"
|
||||||
| "keyUsages"
|
| "keyUsages"
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ describe("CertificateV3Service", () => {
|
|||||||
|
|
||||||
describe("orderCertificateFromProfile", () => {
|
describe("orderCertificateFromProfile", () => {
|
||||||
const mockCertificateOrder = {
|
const mockCertificateOrder = {
|
||||||
identifiers: [{ type: "dns" as const, value: "example.com" }],
|
subjectAlternativeNames: [{ type: "dns" as const, value: "example.com" }],
|
||||||
validity: { ttl: "30d" },
|
validity: { ttl: "30d" },
|
||||||
commonName: "example.com",
|
commonName: "example.com",
|
||||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE],
|
||||||
@@ -411,8 +411,8 @@ describe("CertificateV3Service", () => {
|
|||||||
expect(result).toHaveProperty("orderId");
|
expect(result).toHaveProperty("orderId");
|
||||||
expect(result).toHaveProperty("status", "valid");
|
expect(result).toHaveProperty("status", "valid");
|
||||||
expect(result).toHaveProperty("certificate");
|
expect(result).toHaveProperty("certificate");
|
||||||
expect(result.identifiers).toHaveLength(1);
|
expect(result.subjectAlternativeNames).toHaveLength(1);
|
||||||
expect(result.identifiers[0]).toEqual({
|
expect(result.subjectAlternativeNames[0]).toEqual({
|
||||||
type: "dns",
|
type: "dns",
|
||||||
value: "example.com",
|
value: "example.com",
|
||||||
status: "valid"
|
status: "valid"
|
||||||
|
|||||||
@@ -293,9 +293,9 @@ export const certificateV3ServiceFactory = ({
|
|||||||
commonName: certificateOrder.commonName,
|
commonName: certificateOrder.commonName,
|
||||||
keyUsages: certificateOrder.keyUsages,
|
keyUsages: certificateOrder.keyUsages,
|
||||||
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
extendedKeyUsages: certificateOrder.extendedKeyUsages,
|
||||||
subjectAlternativeNames: certificateOrder.identifiers.map((id) => ({
|
subjectAlternativeNames: certificateOrder.subjectAlternativeNames.map((san) => ({
|
||||||
type: id.type === "dns" ? ("dns_name" as const) : ("ip_address" as const),
|
type: san.type === "dns" ? ("dns_name" as const) : ("ip_address" as const),
|
||||||
value: id.value
|
value: san.value
|
||||||
})),
|
})),
|
||||||
validity: certificateOrder.validity,
|
validity: certificateOrder.validity,
|
||||||
notBefore: certificateOrder.notBefore,
|
notBefore: certificateOrder.notBefore,
|
||||||
@@ -334,16 +334,16 @@ export const certificateV3ServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const orderId = randomUUID();
|
const orderId = randomUUID();
|
||||||
const identifiers = certificateOrder.identifiers.map((id) => ({
|
const subjectAlternativeNames = certificateOrder.subjectAlternativeNames.map((san) => ({
|
||||||
type: id.type,
|
type: san.type,
|
||||||
value: id.value,
|
value: san.value,
|
||||||
status: "valid" as const
|
status: "valid" as const
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const authorizations = certificateOrder.identifiers.map((id) => ({
|
const authorizations = certificateOrder.subjectAlternativeNames.map((san) => ({
|
||||||
identifier: {
|
identifier: {
|
||||||
type: id.type,
|
type: san.type,
|
||||||
value: id.value
|
value: san.value
|
||||||
},
|
},
|
||||||
status: "valid" as const,
|
status: "valid" as const,
|
||||||
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
||||||
@@ -360,7 +360,7 @@ export const certificateV3ServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
orderId,
|
orderId,
|
||||||
status: "valid",
|
status: "valid",
|
||||||
identifiers,
|
subjectAlternativeNames,
|
||||||
authorizations,
|
authorizations,
|
||||||
finalize: `/api/v3/certificates/orders/${orderId}/finalize`,
|
finalize: `/api/v3/certificates/orders/${orderId}/finalize`,
|
||||||
certificate: certificateResult.certificate
|
certificate: certificateResult.certificate
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export type TSignCertificateFromProfileDTO = {
|
|||||||
export type TOrderCertificateFromProfileDTO = {
|
export type TOrderCertificateFromProfileDTO = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
certificateOrder: {
|
certificateOrder: {
|
||||||
identifiers: Array<{
|
subjectAlternativeNames: Array<{
|
||||||
type: "dns" | "ip";
|
type: "dns" | "ip";
|
||||||
value: string;
|
value: string;
|
||||||
}>;
|
}>;
|
||||||
@@ -64,7 +64,7 @@ export type TCertificateFromProfileResponse = {
|
|||||||
export type TCertificateOrderResponse = {
|
export type TCertificateOrderResponse = {
|
||||||
orderId: string;
|
orderId: string;
|
||||||
status: "pending" | "processing" | "valid" | "invalid";
|
status: "pending" | "processing" | "valid" | "invalid";
|
||||||
identifiers: Array<{
|
subjectAlternativeNames: Array<{
|
||||||
type: "dns" | "ip";
|
type: "dns" | "ip";
|
||||||
value: string;
|
value: string;
|
||||||
status: "pending" | "processing" | "valid" | "invalid";
|
status: "pending" | "processing" | "valid" | "invalid";
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { TApiEnrollmentConfigInsert, TApiEnrollmentConfigUpdate } from "./enroll
|
|||||||
export type TApiEnrollmentConfigDALFactory = ReturnType<typeof apiEnrollmentConfigDALFactory>;
|
export type TApiEnrollmentConfigDALFactory = ReturnType<typeof apiEnrollmentConfigDALFactory>;
|
||||||
|
|
||||||
export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
|
export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
|
||||||
const apiEnrollmentConfigOrm = ormify(db, TableName.ApiEnrollmentConfig);
|
const apiEnrollmentConfigOrm = ormify(db, TableName.PkiApiEnrollmentConfig);
|
||||||
|
|
||||||
const create = async (data: TApiEnrollmentConfigInsert, tx?: Knex) => {
|
const create = async (data: TApiEnrollmentConfigInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [apiConfig] = await (tx || db)(TableName.ApiEnrollmentConfig).insert(data).returning("*");
|
const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).insert(data).returning("*");
|
||||||
|
|
||||||
return apiConfig;
|
return apiConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -24,7 +24,7 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const updateById = async (id: string, data: TApiEnrollmentConfigUpdate, tx?: Knex) => {
|
const updateById = async (id: string, data: TApiEnrollmentConfigUpdate, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [apiConfig] = await (tx || db)(TableName.ApiEnrollmentConfig).where({ id }).update(data).returning("*");
|
const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).update(data).returning("*");
|
||||||
|
|
||||||
return apiConfig;
|
return apiConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -34,7 +34,7 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const deleteById = async (id: string, tx?: Knex) => {
|
const deleteById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [apiConfig] = await (tx || db)(TableName.ApiEnrollmentConfig).where({ id }).del().returning("*");
|
const [apiConfig] = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).del().returning("*");
|
||||||
|
|
||||||
return apiConfig;
|
return apiConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -44,7 +44,7 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const apiConfig = await (tx || db)(TableName.ApiEnrollmentConfig).where({ id }).first();
|
const apiConfig = await (tx || db)(TableName.PkiApiEnrollmentConfig).where({ id }).first();
|
||||||
|
|
||||||
return apiConfig;
|
return apiConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -60,15 +60,15 @@ export const apiEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const profiles = await (tx || db)(TableName.CertificateProfile)
|
const profiles = await (tx || db)(TableName.CertificateProfile)
|
||||||
.join(
|
.join(
|
||||||
TableName.ApiEnrollmentConfig,
|
TableName.PkiApiEnrollmentConfig,
|
||||||
`${TableName.CertificateProfile}.apiConfigId`,
|
`${TableName.CertificateProfile}.apiConfigId`,
|
||||||
`${TableName.ApiEnrollmentConfig}.id`
|
`${TableName.PkiApiEnrollmentConfig}.id`
|
||||||
)
|
)
|
||||||
.where(`${TableName.ApiEnrollmentConfig}.autoRenew`, true)
|
.where(`${TableName.PkiApiEnrollmentConfig}.autoRenew`, true)
|
||||||
.where((query) => {
|
.where((query) => {
|
||||||
void query
|
void query
|
||||||
.whereNull(`${TableName.ApiEnrollmentConfig}.autoRenewDays`)
|
.whereNull(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`)
|
||||||
.orWhere(`${TableName.ApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays);
|
.orWhere(`${TableName.PkiApiEnrollmentConfig}.autoRenewDays`, "<=", renewalThresholdDays);
|
||||||
})
|
})
|
||||||
.select((tx || db).ref("id").withSchema(TableName.CertificateProfile))
|
.select((tx || db).ref("id").withSchema(TableName.CertificateProfile))
|
||||||
.select((tx || db).ref("name").withSchema(TableName.CertificateProfile))
|
.select((tx || db).ref("name").withSchema(TableName.CertificateProfile))
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import {
|
import {
|
||||||
TApiEnrollmentConfigs,
|
TPkiApiEnrollmentConfigs,
|
||||||
TApiEnrollmentConfigsInsert,
|
TPkiApiEnrollmentConfigsInsert,
|
||||||
TApiEnrollmentConfigsUpdate
|
TPkiApiEnrollmentConfigsUpdate
|
||||||
} from "@app/db/schemas/api-enrollment-configs";
|
} from "@app/db/schemas/pki-api-enrollment-configs";
|
||||||
import {
|
import {
|
||||||
TEstEnrollmentConfigs,
|
TPkiEstEnrollmentConfigs,
|
||||||
TEstEnrollmentConfigsInsert,
|
TPkiEstEnrollmentConfigsInsert,
|
||||||
TEstEnrollmentConfigsUpdate
|
TPkiEstEnrollmentConfigsUpdate
|
||||||
} from "@app/db/schemas/est-enrollment-configs";
|
} from "@app/db/schemas/pki-est-enrollment-configs";
|
||||||
|
|
||||||
export type TEstEnrollmentConfig = TEstEnrollmentConfigs;
|
export type TEstEnrollmentConfig = TPkiEstEnrollmentConfigs;
|
||||||
export type TEstEnrollmentConfigInsert = TEstEnrollmentConfigsInsert;
|
export type TEstEnrollmentConfigInsert = TPkiEstEnrollmentConfigsInsert;
|
||||||
export type TEstEnrollmentConfigUpdate = TEstEnrollmentConfigsUpdate;
|
export type TEstEnrollmentConfigUpdate = TPkiEstEnrollmentConfigsUpdate;
|
||||||
|
|
||||||
export type TApiEnrollmentConfig = TApiEnrollmentConfigs;
|
export type TApiEnrollmentConfig = TPkiApiEnrollmentConfigs;
|
||||||
export type TApiEnrollmentConfigInsert = TApiEnrollmentConfigsInsert;
|
export type TApiEnrollmentConfigInsert = TPkiApiEnrollmentConfigsInsert;
|
||||||
export type TApiEnrollmentConfigUpdate = TApiEnrollmentConfigsUpdate;
|
export type TApiEnrollmentConfigUpdate = TPkiApiEnrollmentConfigsUpdate;
|
||||||
|
|
||||||
export interface TEstConfigData {
|
export interface TEstConfigData {
|
||||||
disableBootstrapCaValidation: boolean;
|
disableBootstrapCaValidation: boolean;
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { TEstEnrollmentConfigInsert, TEstEnrollmentConfigUpdate } from "./enroll
|
|||||||
export type TEstEnrollmentConfigDALFactory = ReturnType<typeof estEnrollmentConfigDALFactory>;
|
export type TEstEnrollmentConfigDALFactory = ReturnType<typeof estEnrollmentConfigDALFactory>;
|
||||||
|
|
||||||
export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
||||||
const estEnrollmentConfigOrm = ormify(db, TableName.EstEnrollmentConfig);
|
const estEnrollmentConfigOrm = ormify(db, TableName.PkiEstEnrollmentConfig);
|
||||||
|
|
||||||
const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => {
|
const create = async (data: TEstEnrollmentConfigInsert, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [estConfig] = await (tx || db)(TableName.EstEnrollmentConfig).insert(data).returning("*");
|
const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).insert(data).returning("*");
|
||||||
|
|
||||||
return estConfig;
|
return estConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -24,7 +24,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => {
|
const updateById = async (id: string, data: TEstEnrollmentConfigUpdate, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [estConfig] = await (tx || db)(TableName.EstEnrollmentConfig).where({ id }).update(data).returning("*");
|
const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).update(data).returning("*");
|
||||||
|
|
||||||
return estConfig;
|
return estConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -34,7 +34,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const deleteById = async (id: string, tx?: Knex) => {
|
const deleteById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const [estConfig] = await (tx || db)(TableName.EstEnrollmentConfig).where({ id }).del().returning("*");
|
const [estConfig] = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).del().returning("*");
|
||||||
|
|
||||||
return estConfig;
|
return estConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -44,7 +44,7 @@ export const estEnrollmentConfigDALFactory = (db: TDbClient) => {
|
|||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
const findById = async (id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const estConfig = await (tx || db)(TableName.EstEnrollmentConfig).where({ id }).first();
|
const estConfig = await (tx || db)(TableName.PkiEstEnrollmentConfig).where({ id }).first();
|
||||||
|
|
||||||
return estConfig;
|
return estConfig;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export type TCertificateProfile = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
caId: string;
|
caId: string;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
name: string;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enrollmentType: "api" | "est";
|
enrollmentType: "api" | "est";
|
||||||
@@ -24,7 +23,7 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
|
|||||||
certificateTemplate?: {
|
certificateTemplate?: {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
@@ -44,7 +43,6 @@ export type TCreateCertificateProfileDTO = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
caId: string;
|
caId: string;
|
||||||
certificateTemplateId: string;
|
certificateTemplateId: string;
|
||||||
name: string;
|
|
||||||
slug: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
enrollmentType: "api" | "est";
|
enrollmentType: "api" | "est";
|
||||||
@@ -61,7 +59,7 @@ export type TCreateCertificateProfileDTO = {
|
|||||||
|
|
||||||
export type TUpdateCertificateProfileDTO = {
|
export type TUpdateCertificateProfileDTO = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
name?: string;
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
estConfig?: {
|
estConfig?: {
|
||||||
disableBootstrapCaValidation?: boolean;
|
disableBootstrapCaValidation?: boolean;
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export type TCertificateTemplateV2Policy = {
|
|||||||
export type TCertificateTemplateV2New = {
|
export type TCertificateTemplateV2New = {
|
||||||
id: string;
|
id: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
attributes: any;
|
attributes: any;
|
||||||
keyUsages: any;
|
keyUsages: any;
|
||||||
@@ -182,7 +182,7 @@ export type TCertificateTemplateV2New = {
|
|||||||
|
|
||||||
export type TCreateCertificateTemplateV2NewDTO = {
|
export type TCreateCertificateTemplateV2NewDTO = {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
name: string;
|
slug: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
attributes: TCertificateTemplateV2Policy["attributes"];
|
attributes: TCertificateTemplateV2Policy["attributes"];
|
||||||
keyUsages: TCertificateTemplateV2Policy["keyUsages"];
|
keyUsages: TCertificateTemplateV2Policy["keyUsages"];
|
||||||
@@ -195,7 +195,7 @@ export type TCreateCertificateTemplateV2NewDTO = {
|
|||||||
|
|
||||||
export type TUpdateCertificateTemplateV2NewDTO = {
|
export type TUpdateCertificateTemplateV2NewDTO = {
|
||||||
templateId: string;
|
templateId: string;
|
||||||
name?: string;
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
attributes?: TCertificateTemplateV2Policy["attributes"];
|
attributes?: TCertificateTemplateV2Policy["attributes"];
|
||||||
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
keyUsages?: TCertificateTemplateV2Policy["keyUsages"];
|
||||||
|
|||||||
+65
-98
@@ -15,6 +15,7 @@ import {
|
|||||||
AccordionItem,
|
AccordionItem,
|
||||||
AccordionTrigger,
|
AccordionTrigger,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -26,7 +27,7 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { useCreateCertificateV3, useGetCert, useListWorkspacePkiCollections } from "@app/hooks/api";
|
import { useCreateCertificateV3, useGetCert } from "@app/hooks/api";
|
||||||
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
import { useListCertificateProfiles } from "@app/hooks/api/certificateProfiles";
|
||||||
import {
|
import {
|
||||||
certKeyAlgorithms,
|
certKeyAlgorithms,
|
||||||
@@ -44,53 +45,15 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
|
|
||||||
type TriStateToggleProps = {
|
|
||||||
value: boolean | undefined;
|
|
||||||
onChange: (value: boolean | undefined) => void;
|
|
||||||
leftLabel: string;
|
|
||||||
rightLabel: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TriStateToggle = ({ value, onChange, leftLabel, rightLabel }: TriStateToggleProps) => {
|
|
||||||
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 === false ? undefined : false);
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
className={`${
|
|
||||||
value === false ? "bg-mineshaft-500" : "bg-transparent"
|
|
||||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
|
||||||
>
|
|
||||||
{leftLabel}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => {
|
|
||||||
onChange(value === true ? undefined : true);
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
className={`${
|
|
||||||
value === true ? "bg-mineshaft-500" : "bg-transparent"
|
|
||||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
|
||||||
>
|
|
||||||
{rightLabel}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
profileId: z.string().min(1, "Profile is required"),
|
profileId: z.string().min(1, "Profile is required"),
|
||||||
collectionId: z.string().optional(),
|
|
||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
subjectAttributes: z
|
subjectAttributes: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["common_name"]),
|
type: z.enum(["common_name"]),
|
||||||
value: z.string().min(1, "Value is required")
|
value: z.string().min(1, "Value is required"),
|
||||||
|
include: z.enum(["mandatory", "optional", "prohibit"]).optional()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1, "At least one subject attribute is required"),
|
.min(1, "At least one subject attribute is required"),
|
||||||
@@ -98,7 +61,8 @@ const schema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
type: z.enum(["dns", "ip", "email", "uri"]),
|
type: z.enum(["dns", "ip", "email", "uri"]),
|
||||||
value: z.string().min(1, "Value is required")
|
value: z.string().min(1, "Value is required"),
|
||||||
|
include: z.enum(["mandatory", "optional", "prohibit"]).optional()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.default([]),
|
.default([]),
|
||||||
@@ -159,10 +123,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
includeMetrics: false
|
includeMetrics: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: collectionsData } = useListWorkspacePkiCollections({
|
|
||||||
projectId: currentProject?.id || ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
const { mutateAsync: createCertificate } = useCreateCertificateV3();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -461,7 +421,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
profileId,
|
profileId,
|
||||||
friendlyName,
|
friendlyName,
|
||||||
collectionId,
|
|
||||||
subjectAttributes,
|
subjectAttributes,
|
||||||
altNames,
|
altNames,
|
||||||
ttl,
|
ttl,
|
||||||
@@ -481,7 +440,6 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({
|
||||||
profileId,
|
profileId,
|
||||||
projectSlug: currentProject.slug,
|
projectSlug: currentProject.slug,
|
||||||
pkiCollectionId: collectionId,
|
|
||||||
friendlyName,
|
friendlyName,
|
||||||
commonName: getAttributeValue("common_name"),
|
commonName: getAttributeValue("common_name"),
|
||||||
altNames: altNames
|
altNames: altNames
|
||||||
@@ -628,34 +586,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
>
|
>
|
||||||
{profilesData?.certificateProfiles?.map((profile) => (
|
{profilesData?.certificateProfiles?.map((profile) => (
|
||||||
<SelectItem key={profile.id} value={profile.id}>
|
<SelectItem key={profile.id} value={profile.id}>
|
||||||
{profile.name}
|
{profile.slug}
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="collectionId"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label="PKI Collection (Optional)"
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue=""
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Select a collection (optional)"
|
|
||||||
position="popper"
|
|
||||||
>
|
|
||||||
{collectionsData?.collections?.map((collection: any) => (
|
|
||||||
<SelectItem key={collection.id} value={collection.id}>
|
|
||||||
{collection.name}
|
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -701,10 +632,25 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}}
|
}}
|
||||||
className="w-48"
|
className="w-48"
|
||||||
position="popper"
|
|
||||||
>
|
>
|
||||||
<SelectItem value="common_name">Common Name</SelectItem>
|
<SelectItem value="common_name">Common Name</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={attr.include || "optional"}
|
||||||
|
onValueChange={(newInclude) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue[index] = {
|
||||||
|
...attr,
|
||||||
|
include: newInclude as "mandatory" | "optional" | "prohibit"
|
||||||
|
};
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
className="w-32"
|
||||||
|
>
|
||||||
|
<SelectItem value="mandatory">Mandatory</SelectItem>
|
||||||
|
<SelectItem value="optional">Optional</SelectItem>
|
||||||
|
<SelectItem value="prohibit">Prohibited</SelectItem>
|
||||||
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
value={attr.value}
|
value={attr.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -769,14 +715,29 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
};
|
};
|
||||||
onChange(newValue);
|
onChange(newValue);
|
||||||
}}
|
}}
|
||||||
className="w-32"
|
className="w-24"
|
||||||
position="popper"
|
|
||||||
>
|
>
|
||||||
<SelectItem value="dns">DNS</SelectItem>
|
<SelectItem value="dns">DNS</SelectItem>
|
||||||
<SelectItem value="ip">IP</SelectItem>
|
<SelectItem value="ip">IP</SelectItem>
|
||||||
<SelectItem value="email">Email</SelectItem>
|
<SelectItem value="email">Email</SelectItem>
|
||||||
<SelectItem value="uri">URI</SelectItem>
|
<SelectItem value="uri">URI</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={san.include || "optional"}
|
||||||
|
onValueChange={(newInclude) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue[index] = {
|
||||||
|
...san,
|
||||||
|
include: newInclude as "mandatory" | "optional" | "prohibit"
|
||||||
|
};
|
||||||
|
onChange(newValue);
|
||||||
|
}}
|
||||||
|
className="w-32"
|
||||||
|
>
|
||||||
|
<SelectItem value="mandatory">Mandatory</SelectItem>
|
||||||
|
<SelectItem value="optional">Optional</SelectItem>
|
||||||
|
<SelectItem value="prohibit">Prohibited</SelectItem>
|
||||||
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
value={san.value}
|
value={san.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -908,11 +869,11 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Accordion type="single" collapsible>
|
<Accordion type="single" collapsible className="w-full">
|
||||||
<AccordionItem value="key-usages">
|
<AccordionItem value="key-usages">
|
||||||
<AccordionTrigger>Key Usages</AccordionTrigger>
|
<AccordionTrigger>Key Usages</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||||
{KEY_USAGES_OPTIONS.filter(({ value }) => {
|
{KEY_USAGES_OPTIONS.filter(({ value }) => {
|
||||||
if (allowedKeyUsages.length === 0) return true;
|
if (allowedKeyUsages.length === 0) return true;
|
||||||
const templateToEnumMap = {
|
const templateToEnumMap = {
|
||||||
@@ -933,15 +894,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
<Controller
|
<Controller
|
||||||
key={label}
|
key={label}
|
||||||
control={control}
|
control={control}
|
||||||
name={`keyUsages.${value}`}
|
name={`keyUsages.${value}` as any}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center space-x-3">
|
||||||
<span className="text-sm text-mineshaft-300">{label}</span>
|
<Checkbox
|
||||||
<TriStateToggle
|
id={`key-usage-${value}`}
|
||||||
value={field.value}
|
isChecked={field.value || false}
|
||||||
onChange={field.onChange}
|
onCheckedChange={(checked) => field.onChange(checked)}
|
||||||
leftLabel="None"
|
/>
|
||||||
rightLabel="Include"
|
<FormLabel
|
||||||
|
id={`key-usage-${value}`}
|
||||||
|
className="cursor-pointer text-sm text-mineshaft-300"
|
||||||
|
label={label}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -954,7 +918,7 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
<AccordionItem value="extended-key-usages">
|
<AccordionItem value="extended-key-usages">
|
||||||
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
<AccordionTrigger>Extended Key Usages</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<div className="grid grid-cols-1 gap-3">
|
<div className="grid grid-cols-2 gap-2 pl-2">
|
||||||
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => {
|
{EXTENDED_KEY_USAGES_OPTIONS.filter(({ value }) => {
|
||||||
if (allowedExtendedKeyUsages.length === 0) return true;
|
if (allowedExtendedKeyUsages.length === 0) return true;
|
||||||
const templateToEnumMap = {
|
const templateToEnumMap = {
|
||||||
@@ -973,15 +937,18 @@ export const CertificateIssuanceModal = ({ popUp, handlePopUpToggle }: Props) =>
|
|||||||
<Controller
|
<Controller
|
||||||
key={label}
|
key={label}
|
||||||
control={control}
|
control={control}
|
||||||
name={`extendedKeyUsages.${value}`}
|
name={`extendedKeyUsages.${value}` as any}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center space-x-3">
|
||||||
<span className="text-sm text-mineshaft-300">{label}</span>
|
<Checkbox
|
||||||
<TriStateToggle
|
id={`ext-key-usage-${value}`}
|
||||||
value={field.value}
|
isChecked={field.value || false}
|
||||||
onChange={field.onChange}
|
onCheckedChange={(checked) => field.onChange(checked)}
|
||||||
leftLabel="None"
|
/>
|
||||||
rightLabel="Include"
|
<FormLabel
|
||||||
|
id={`ext-key-usage-${value}`}
|
||||||
|
className="cursor-pointer text-sm text-mineshaft-300"
|
||||||
|
label={label}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+4
-4
@@ -14,7 +14,6 @@ import {
|
|||||||
} from "@app/hooks/api/certificateProfiles";
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
|
|
||||||
import { CreateProfileModal } from "./CreateProfileModal";
|
import { CreateProfileModal } from "./CreateProfileModal";
|
||||||
import { EditProfileModal } from "./EditProfileModal";
|
|
||||||
import { ProfileList } from "./ProfileList";
|
import { ProfileList } from "./ProfileList";
|
||||||
|
|
||||||
export const CertificateProfilesTab = () => {
|
export const CertificateProfilesTab = () => {
|
||||||
@@ -89,23 +88,24 @@ export const CertificateProfilesTab = () => {
|
|||||||
|
|
||||||
{selectedProfile && (
|
{selectedProfile && (
|
||||||
<>
|
<>
|
||||||
<EditProfileModal
|
<CreateProfileModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsEditModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
setSelectedProfile(null);
|
setSelectedProfile(null);
|
||||||
}}
|
}}
|
||||||
profile={selectedProfile}
|
profile={selectedProfile}
|
||||||
|
mode="edit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
title={`Delete Certificate Profile ${selectedProfile.name}?`}
|
title={`Delete Certificate Profile ${selectedProfile.slug}?`}
|
||||||
onChange={(isOpen) => {
|
onChange={(isOpen) => {
|
||||||
setIsDeleteModalOpen(isOpen);
|
setIsDeleteModalOpen(isOpen);
|
||||||
if (!isOpen) setSelectedProfile(null);
|
if (!isOpen) setSelectedProfile(null);
|
||||||
}}
|
}}
|
||||||
deleteKey={selectedProfile.name}
|
deleteKey={selectedProfile.slug}
|
||||||
onDeleteApproved={handleDeleteConfirm}
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+145
-87
@@ -1,5 +1,3 @@
|
|||||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -18,12 +16,15 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useProject } from "@app/context";
|
import { useProject } from "@app/context";
|
||||||
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
import { useListCasByProjectId } from "@app/hooks/api/ca/queries";
|
||||||
import { useCreateCertificateProfile } from "@app/hooks/api/certificateProfiles";
|
import {
|
||||||
|
TCertificateProfileWithDetails,
|
||||||
|
useCreateCertificateProfile,
|
||||||
|
useUpdateCertificateProfile
|
||||||
|
} from "@app/hooks/api/certificateProfiles";
|
||||||
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
import { useListCertificateTemplatesV2 } from "@app/hooks/api/certificateTemplates/queries";
|
||||||
|
|
||||||
const schema = z
|
const createSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().trim().min(1, "Profile name is required"),
|
|
||||||
slug: z.string().trim().min(1, "Profile slug is required"),
|
slug: z.string().trim().min(1, "Profile slug is required"),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
enrollmentType: z.enum(["api", "est"]),
|
enrollmentType: z.enum(["api", "est"]),
|
||||||
@@ -58,14 +59,52 @@ const schema = z
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
const editSchema = z
|
||||||
|
.object({
|
||||||
|
slug: z.string().trim().min(1, "Profile slug is required"),
|
||||||
|
description: z.string().optional(),
|
||||||
|
enrollmentType: z.enum(["api", "est"]),
|
||||||
|
certificateAuthorityId: z.string().optional(),
|
||||||
|
certificateTemplateId: z.string().optional(),
|
||||||
|
estConfig: z
|
||||||
|
.object({
|
||||||
|
disableBootstrapCaValidation: z.boolean().optional(),
|
||||||
|
passphrase: z.string().optional(),
|
||||||
|
caChain: z.string().optional()
|
||||||
|
})
|
||||||
|
.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 createSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
profile?: TCertificateProfileWithDetails;
|
||||||
|
mode?: "create" | "edit";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
export const CreateProfileModal = ({ isOpen, onClose, profile, mode = "create" }: Props) => {
|
||||||
const { currentProject } = useProject();
|
const { currentProject } = useProject();
|
||||||
|
|
||||||
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
|
const { data: caData } = useListCasByProjectId(currentProject?.id || "");
|
||||||
@@ -76,80 +115,97 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const createProfile = useCreateCertificateProfile();
|
const createProfile = useCreateCertificateProfile();
|
||||||
|
const updateProfile = useUpdateCertificateProfile();
|
||||||
|
|
||||||
|
const isEdit = mode === "edit" && profile;
|
||||||
|
|
||||||
const certificateAuthorities = caData || [];
|
const certificateAuthorities = caData || [];
|
||||||
const certificateTemplates = templateData?.certificateTemplates || [];
|
const certificateTemplates = templateData?.certificateTemplates || [];
|
||||||
|
|
||||||
const {
|
const { control, handleSubmit, reset, watch, setValue } = useForm<FormData>({
|
||||||
control,
|
resolver: zodResolver(isEdit ? editSchema : createSchema),
|
||||||
handleSubmit,
|
defaultValues: isEdit
|
||||||
reset,
|
? {
|
||||||
watch,
|
slug: profile.slug,
|
||||||
setValue,
|
description: profile.description || "",
|
||||||
formState: { isSubmitting }
|
enrollmentType: profile.enrollmentType,
|
||||||
} = useForm<FormData>({
|
certificateAuthorityId: profile.caId,
|
||||||
resolver: zodResolver(schema),
|
certificateTemplateId: profile.certificateTemplateId,
|
||||||
defaultValues: {
|
estConfig: {
|
||||||
name: "",
|
disableBootstrapCaValidation: profile.estConfig?.disableBootstrapCaValidation || false,
|
||||||
slug: "",
|
passphrase: "",
|
||||||
description: "",
|
caChain: ""
|
||||||
enrollmentType: "api",
|
},
|
||||||
certificateAuthorityId: "",
|
apiConfig: {
|
||||||
certificateTemplateId: "",
|
autoRenew: profile.apiConfig?.autoRenew || false,
|
||||||
apiConfig: {
|
autoRenewDays: profile.apiConfig?.autoRenewDays || 30
|
||||||
autoRenew: false,
|
}
|
||||||
autoRenewDays: 30
|
}
|
||||||
}
|
: {
|
||||||
}
|
slug: "",
|
||||||
|
description: "",
|
||||||
|
enrollmentType: "api",
|
||||||
|
certificateAuthorityId: "",
|
||||||
|
certificateTemplateId: "",
|
||||||
|
apiConfig: {
|
||||||
|
autoRenew: false,
|
||||||
|
autoRenewDays: 30
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const watchedName = watch("name");
|
|
||||||
const watchedEnrollmentType = watch("enrollmentType");
|
const watchedEnrollmentType = watch("enrollmentType");
|
||||||
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
const watchedDisableBootstrapValidation = watch("estConfig.disableBootstrapCaValidation");
|
||||||
const watchedAutoRenew = watch("apiConfig.autoRenew");
|
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) => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentProject?.id) return;
|
if (!currentProject?.id && !isEdit) return;
|
||||||
|
|
||||||
const payload: any = {
|
if (isEdit) {
|
||||||
projectId: currentProject.id,
|
const updateData: any = {
|
||||||
name: data.name,
|
profileId: profile.id,
|
||||||
slug: data.slug,
|
name: data.slug,
|
||||||
description: data.description,
|
description: data.description
|
||||||
enrollmentType: data.enrollmentType,
|
};
|
||||||
caId: data.certificateAuthorityId,
|
|
||||||
certificateTemplateId: data.certificateTemplateId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data.enrollmentType === "est" && data.estConfig) {
|
if (data.enrollmentType === "est" && data.estConfig) {
|
||||||
payload.estConfig = data.estConfig;
|
updateData.estConfig = data.estConfig;
|
||||||
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||||
payload.apiConfig = data.apiConfig;
|
updateData.apiConfig = data.apiConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateProfile.mutateAsync(updateData);
|
||||||
|
} else {
|
||||||
|
const createData: any = {
|
||||||
|
projectId: currentProject!.id,
|
||||||
|
slug: data.slug,
|
||||||
|
description: data.description,
|
||||||
|
enrollmentType: data.enrollmentType,
|
||||||
|
caId: data.certificateAuthorityId,
|
||||||
|
certificateTemplateId: data.certificateTemplateId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.enrollmentType === "est" && data.estConfig) {
|
||||||
|
createData.estConfig = data.estConfig;
|
||||||
|
} else if (data.enrollmentType === "api" && data.apiConfig) {
|
||||||
|
createData.apiConfig = data.apiConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createProfile.mutateAsync(createData);
|
||||||
}
|
}
|
||||||
await createProfile.mutateAsync(payload);
|
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Certificate profile created successfully",
|
text: `Certificate profile ${isEdit ? "updated" : "created"} successfully`,
|
||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating profile:", error);
|
console.error(`Error ${isEdit ? "updating" : "creating"} profile:`, error);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to create certificate profile",
|
text: `Failed to ${isEdit ? "update" : "create"} certificate profile`,
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -166,25 +222,14 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Create Certificate Profile"
|
title={isEdit ? "Edit Certificate Profile" : "Create Certificate Profile"}
|
||||||
subTitle="Configure a new certificate profile for unified certificate issuance"
|
subTitle={
|
||||||
|
isEdit
|
||||||
|
? `Update configuration for ${profile?.slug}`
|
||||||
|
: "Configure a new certificate profile for unified certificate issuance"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="slug"
|
name="slug"
|
||||||
@@ -195,7 +240,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="auto-generated-from-name" />
|
<Input {...field} placeholder="your-profile-name" isDisabled={Boolean(isEdit)} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -226,6 +271,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
placeholder="Select a certificate authority"
|
placeholder="Select a certificate authority"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
position="popper"
|
position="popper"
|
||||||
|
isDisabled={Boolean(isEdit)}
|
||||||
>
|
>
|
||||||
{certificateAuthorities.map((ca: any) => (
|
{certificateAuthorities.map((ca: any) => (
|
||||||
<SelectItem key={ca.id} value={ca.id}>
|
<SelectItem key={ca.id} value={ca.id}>
|
||||||
@@ -269,10 +315,11 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
placeholder="Select a certificate template"
|
placeholder="Select a certificate template"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
position="popper"
|
position="popper"
|
||||||
|
isDisabled={Boolean(isEdit)}
|
||||||
>
|
>
|
||||||
{certificateTemplates.map((template) => (
|
{certificateTemplates.map((template) => (
|
||||||
<SelectItem key={template.id} value={template.id}>
|
<SelectItem key={template.id} value={template.id}>
|
||||||
{template.name}
|
{template.slug}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
@@ -290,7 +337,13 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Select {...field} onValueChange={onChange} className="w-full" position="popper">
|
<Select
|
||||||
|
{...field}
|
||||||
|
onValueChange={onChange}
|
||||||
|
className="w-full"
|
||||||
|
position="popper"
|
||||||
|
isDisabled={Boolean(isEdit)}
|
||||||
|
>
|
||||||
<SelectItem value="api">API</SelectItem>
|
<SelectItem value="api">API</SelectItem>
|
||||||
<SelectItem value="est">EST</SelectItem>
|
<SelectItem value="est">EST</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -314,12 +367,9 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
onCheckedChange={onChange}
|
onCheckedChange={onChange}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<label
|
<span className="text-sm font-medium text-mineshaft-100">
|
||||||
htmlFor="disableBootstrapCaValidation"
|
|
||||||
className="text-sm font-medium text-mineshaft-100"
|
|
||||||
>
|
|
||||||
Disable Bootstrap CA Validation
|
Disable Bootstrap CA Validation
|
||||||
</label>
|
</span>
|
||||||
<p className="text-xs text-bunker-300">
|
<p className="text-xs text-bunker-300">
|
||||||
Skip CA certificate validation during EST bootstrap phase
|
Skip CA certificate validation during EST bootstrap phase
|
||||||
</p>
|
</p>
|
||||||
@@ -335,7 +385,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="EST Passphrase"
|
label="EST Passphrase"
|
||||||
isRequired
|
isRequired={!isEdit}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
@@ -356,7 +406,7 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="CA Chain Certificate"
|
label="CA Chain Certificate"
|
||||||
isRequired
|
isRequired={!isEdit}
|
||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
@@ -424,10 +474,18 @@ export const CreateProfileModal = ({ isOpen, onClose }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button type="submit" colorSchema="primary" isLoading={isSubmitting}>
|
<Button
|
||||||
Create
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
isLoading={isEdit ? updateProfile.isPending : createProfile.isPending}
|
||||||
|
>
|
||||||
|
{isEdit ? "Save Changes" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline_bg" onClick={onClose} disabled={isSubmitting}>
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isEdit ? updateProfile.isPending : createProfile.isPending}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-303
@@ -1,303 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+1
-2
@@ -34,7 +34,7 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
const profiles = data?.certificateProfiles || [];
|
const profiles = data?.certificateProfiles || [];
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <TableSkeleton columns={7} innerKey="certificate-profiles" />;
|
return <TableSkeleton columns={6} innerKey="certificate-profiles" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!profiles || profiles.length === 0) {
|
if (!profiles || profiles.length === 0) {
|
||||||
@@ -51,7 +51,6 @@ export const ProfileList = ({ onEditProfile, onDeleteProfile }: Props) => {
|
|||||||
<Th>Certificate Authority</Th>
|
<Th>Certificate Authority</Th>
|
||||||
<Th>Template</Th>
|
<Th>Template</Th>
|
||||||
<Th>Certificates</Th>
|
<Th>Certificates</Th>
|
||||||
<Th>Created</Th>
|
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
|
|||||||
+26
-50
@@ -57,23 +57,16 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
return <Badge variant={variant}>{label}</Badge>;
|
return <Badge variant={variant}>{label}</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
|
||||||
return new Date(dateString).toLocaleDateString();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr key={profile.id} className="h-10 transition-colors duration-100 hover:bg-mineshaft-700">
|
<Tr key={profile.id} className="h-10 transition-colors duration-100 hover:bg-mineshaft-700">
|
||||||
<Td>
|
<Td>
|
||||||
<div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="font-medium text-mineshaft-100">{profile.slug}</div>
|
||||||
<div className="font-medium text-mineshaft-100">{profile.name}</div>
|
{profile.description && (
|
||||||
{profile.description && (
|
<Tooltip content={profile.description}>
|
||||||
<Tooltip content={profile.description}>
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
</Tooltip>
|
||||||
</Tooltip>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-bunker-300">{profile.slug}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
<Td className="text-center">{getEnrollmentTypeBadge(profile.enrollmentType)}</Td>
|
||||||
@@ -84,64 +77,47 @@ export const ProfileRow = ({ profile, onEditProfile, onDeleteProfile }: Props) =
|
|||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<span className="text-sm text-mineshaft-300">
|
<span className="text-sm text-mineshaft-300">
|
||||||
{templateData?.name || profile.certificateTemplateId}
|
{templateData?.slug || profile.certificateTemplateId}
|
||||||
</span>
|
</span>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex gap-2 text-xs">
|
<div className="flex flex-wrap gap-1">
|
||||||
{profile.metrics ? (
|
{profile.metrics ? (
|
||||||
profile.metrics.totalCertificates === 0 ? (
|
profile.metrics.totalCertificates === 0 ? (
|
||||||
<span className="text-bunker-300">No certificates attached</span>
|
<Badge variant="primary" className="text-xs">
|
||||||
|
No certificates
|
||||||
|
</Badge>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{profile.metrics.activeCertificates > 0 && (
|
{profile.metrics.activeCertificates > 0 && (
|
||||||
<span className="text-green-400">
|
<Badge variant="success" className="text-xs">
|
||||||
{profile.metrics.activeCertificates} active
|
{profile.metrics.activeCertificates} active
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{profile.metrics.expiringCertificates > 0 && (
|
{profile.metrics.expiringCertificates > 0 && (
|
||||||
<>
|
<Badge variant="primary" className="text-xs">
|
||||||
{profile.metrics.activeCertificates > 0 && (
|
{profile.metrics.expiringCertificates} expiring
|
||||||
<span className="text-gray-400">•</span>
|
</Badge>
|
||||||
)}
|
|
||||||
<span className="text-yellow-400">
|
|
||||||
{profile.metrics.expiringCertificates} expiring
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{profile.metrics.expiredCertificates > 0 && (
|
{profile.metrics.expiredCertificates > 0 && (
|
||||||
<>
|
<Badge variant="danger" className="text-xs">
|
||||||
{(profile.metrics.activeCertificates > 0 ||
|
{profile.metrics.expiredCertificates} expired
|
||||||
profile.metrics.expiringCertificates > 0) && (
|
</Badge>
|
||||||
<span className="text-gray-400">•</span>
|
|
||||||
)}
|
|
||||||
<span className="text-red-300">
|
|
||||||
{profile.metrics.expiredCertificates} expired
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{profile.metrics.revokedCertificates > 0 && (
|
{profile.metrics.revokedCertificates > 0 && (
|
||||||
<>
|
<Badge variant="danger" className="text-xs">
|
||||||
{(profile.metrics.activeCertificates > 0 ||
|
{profile.metrics.revokedCertificates} revoked
|
||||||
profile.metrics.expiringCertificates > 0 ||
|
</Badge>
|
||||||
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>
|
<Badge variant="primary" className="text-xs">
|
||||||
|
No metrics
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>
|
|
||||||
<span className="text-sm text-bunker-300">{formatDate(profile.createdAt)}</span>
|
|
||||||
</Td>
|
|
||||||
<Td className="text-right">
|
<Td className="text-right">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
|||||||
-1
@@ -1,4 +1,3 @@
|
|||||||
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
export { CertificateProfilesTab } from "./CertificateProfilesTab";
|
||||||
export { CreateProfileModal } from "./CreateProfileModal";
|
export { CreateProfileModal } from "./CreateProfileModal";
|
||||||
export { EditProfileModal } from "./EditProfileModal";
|
|
||||||
export { ProfileList } from "./ProfileList";
|
export { ProfileList } from "./ProfileList";
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,6 @@ import { useDeleteCertificateTemplateV2New } from "@app/hooks/api/certificateTem
|
|||||||
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
import { TCertificateTemplateV2New } from "@app/hooks/api/certificateTemplates/types";
|
||||||
|
|
||||||
import { CreateTemplateModal } from "./CreateTemplateModal";
|
import { CreateTemplateModal } from "./CreateTemplateModal";
|
||||||
import { EditTemplateModal } from "./EditTemplateModal";
|
|
||||||
import { TemplateList } from "./TemplateList";
|
import { TemplateList } from "./TemplateList";
|
||||||
|
|
||||||
export const CertificateTemplatesV2Tab = () => {
|
export const CertificateTemplatesV2Tab = () => {
|
||||||
@@ -87,23 +86,24 @@ export const CertificateTemplatesV2Tab = () => {
|
|||||||
|
|
||||||
{selectedTemplate && (
|
{selectedTemplate && (
|
||||||
<>
|
<>
|
||||||
<EditTemplateModal
|
<CreateTemplateModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsEditModalOpen(false);
|
setIsEditModalOpen(false);
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null);
|
||||||
}}
|
}}
|
||||||
template={selectedTemplate}
|
template={selectedTemplate}
|
||||||
|
mode="edit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
title={`Delete Certificate Template ${selectedTemplate.name}?`}
|
title={`Delete Certificate Template ${selectedTemplate.slug}?`}
|
||||||
onChange={(isOpen) => {
|
onChange={(isOpen) => {
|
||||||
setIsDeleteModalOpen(isOpen);
|
setIsDeleteModalOpen(isOpen);
|
||||||
if (!isOpen) setSelectedTemplate(null);
|
if (!isOpen) setSelectedTemplate(null);
|
||||||
}}
|
}}
|
||||||
deleteKey={selectedTemplate.name}
|
deleteKey={selectedTemplate.slug}
|
||||||
onDeleteApproved={handleDeleteConfirm}
|
onDeleteApproved={handleDeleteConfirm}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+585
-501
File diff suppressed because it is too large
Load Diff
-778
@@ -1,778 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+1
-1
@@ -82,7 +82,7 @@ export const TemplateList = ({ onEditTemplate, onDeleteTemplate }: Props) => {
|
|||||||
>
|
>
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="font-medium">{template.name}</div>
|
<div className="font-medium">{template.slug}</div>
|
||||||
{template.description && (
|
{template.description && (
|
||||||
<Tooltip content={template.description}>
|
<Tooltip content={template.description}>
|
||||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||||
|
|||||||
-1
@@ -1,4 +1,3 @@
|
|||||||
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
export { CertificateTemplatesV2Tab } from "./CertificateTemplatesV2Tab";
|
||||||
export { CreateTemplateModal } from "./CreateTemplateModal";
|
export { CreateTemplateModal } from "./CreateTemplateModal";
|
||||||
export { EditTemplateModal } from "./EditTemplateModal";
|
|
||||||
export { TemplateList } from "./TemplateList";
|
export { TemplateList } from "./TemplateList";
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ export const sanSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const templateSchema = z.object({
|
export const templateSchema = z.object({
|
||||||
name: z.string().trim().min(1, "Template name is required"),
|
slug: z.string().trim().min(1, "Template name is required"),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
attributes: z.array(attributeSchema).optional(),
|
attributes: z.array(attributeSchema).optional(),
|
||||||
keyUsages: z
|
keyUsages: z
|
||||||
|
|||||||
Reference in New Issue
Block a user