PKI revamp: UI improvements

This commit is contained in:
Carlos Monastyrski
2025-10-10 19:49:15 -03:00
parent ef9a1961f1
commit 8905885254
45 changed files with 1156 additions and 2089 deletions
@@ -139,7 +139,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
if (search) {
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) {
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 {
const certificateTemplateV2 = await (tx || db)(TableName.CertificateTemplateV2)
.where({ name, projectId })
.where({ slug, projectId })
.first();
if (!certificateTemplateV2) {
@@ -188,7 +188,7 @@ export const certificateTemplateV2DALFactory = (db: TDbClient) => {
return parseJsonFields(certificateTemplateV2);
} 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,
findByProjectId,
countByProjectId,
findByNameAndProjectId,
findBySlugAndProjectId,
isTemplateInUse
};
};
@@ -1,3 +1,4 @@
import RE2 from "re2";
import { z } from "zod";
const attributeTypeSchema = z.enum(["common_name"]);
@@ -87,7 +88,11 @@ export const templateV2KeyAlgorithmSchema = z.object({
export const createCertificateTemplateV2Schema = z.object({
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(),
attributes: z.array(templateV2AttributeSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
@@ -99,7 +104,12 @@ export const createCertificateTemplateV2Schema = 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(),
attributes: z.array(templateV2AttributeSchema).optional(),
keyUsages: templateV2KeyUsagesSchema.optional(),
@@ -114,6 +124,11 @@ export const getCertificateTemplateV2ByIdSchema = z.object({
id: z.string().uuid()
});
export const getCertificateTemplateV2BySlugSchema = z.object({
projectId: z.string().min(1),
slug: z.string().min(1)
});
export const listCertificateTemplatesV2Schema = z.object({
projectId: z.string().min(1),
offset: z.coerce.number().min(0).default(0),
@@ -27,6 +27,7 @@ describe("CertificateTemplateV2Service", () => {
let service: TCertificateTemplateV2ServiceFactory;
const mockCertificateTemplateV2DAL = {
findBySlugAndProjectId: vi.fn(),
create: vi.fn(),
findById: vi.fn(),
updateById: vi.fn(),
@@ -99,7 +100,7 @@ describe("CertificateTemplateV2Service", () => {
const sampleTemplate: TCertificateTemplateV2 = {
id: "template-123",
projectId: "project-123",
name: "Web Server Template",
slug: "web-server-template",
description: "Template for web server certificates",
...samplePolicy,
createdAt: new Date(),
@@ -136,6 +137,7 @@ describe("CertificateTemplateV2Service", () => {
});
mockCertificateTemplateV2DAL.findByNameAndProjectId.mockResolvedValue(null);
mockCertificateTemplateV2DAL.findBySlugAndProjectId.mockResolvedValue(null);
service = certificateTemplateV2ServiceFactory({
certificateTemplateV2DAL: mockCertificateTemplateV2DAL as TCertificateTemplateV2DALFactory,
@@ -149,7 +151,7 @@ describe("CertificateTemplateV2Service", () => {
describe("createTemplateV2", () => {
const createData: Omit<TCertificateTemplateV2Insert, "projectId"> = {
name: "Test Template",
slug: "test-template",
description: "Test description",
...samplePolicy
};
@@ -239,7 +241,7 @@ describe("CertificateTemplateV2Service", () => {
describe("updateTemplateV2", () => {
it("should update template with valid data", async () => {
const updateData = { name: "Updated Template Name" };
const updateData = { slug: "updated-template-name" };
const updatedTemplate = { ...sampleTemplate, ...updateData };
mockCertificateTemplateV2DAL.findById.mockResolvedValue(sampleTemplate);
@@ -263,7 +265,7 @@ describe("CertificateTemplateV2Service", () => {
service.updateTemplateV2({
...mockActor,
templateId: "nonexistent-template",
data: { name: "Updated Name" }
data: { slug: "updated-name" }
})
).rejects.toThrow(NotFoundError);
});
@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import slugify from "@sindresorhus/slugify";
import RE2 from "re2";
import { ActionProjectType } from "@app/db/schemas";
@@ -8,6 +9,7 @@ import {
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
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 = (
template: TCertificateTemplateV2,
request: TCertificateRequest
@@ -361,8 +385,12 @@ export const certificateTemplateV2ServiceFactory = ({
keyAlgorithm: data.keyAlgorithm
});
const slug = data.slug || generateTemplateSlug();
const uniqueSlug = await ensureUniqueSlug(projectId, slug);
const template = await certificateTemplateV2DAL.create({
...data,
slug: uniqueSlug,
projectId
});
@@ -417,7 +445,13 @@ export const certificateTemplateV2ServiceFactory = ({
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) {
throw new NotFoundError({ message: "Failed to update certificate template" });
}
@@ -459,6 +493,43 @@ export const certificateTemplateV2ServiceFactory = ({
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 ({
actor,
actorId,
@@ -571,6 +642,7 @@ export const certificateTemplateV2ServiceFactory = ({
createTemplateV2,
updateTemplateV2,
getTemplateV2ById,
getTemplateV2BySlug,
listTemplatesV2,
deleteTemplateV2,
validateCertificateRequest
@@ -74,7 +74,7 @@ export type TCertificateTemplateV2Insert = Omit<
export type TCertificateTemplateV2Update = Partial<
Pick<
TCertificateTemplateV2,
| "name"
| "slug"
| "description"
| "attributes"
| "keyUsages"