From f5322abe8587853c0f963b7101732d70eb9dcc95 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 12 Jun 2024 13:11:21 -0700 Subject: [PATCH] Resolve PR issues --- backend/src/@types/knex.d.ts | 14 +- .../20240607032218_certificate-mgmt.ts | 13 +- .../src/db/schemas/certificate-authorities.ts | 1 + ...ificate-certs.ts => certificate-bodies.ts} | 8 +- backend/src/db/schemas/certificates.ts | 1 + backend/src/db/schemas/index.ts | 2 +- backend/src/db/schemas/models.ts | 2 +- backend/src/server/routes/index.ts | 8 +- .../routes/v1/certificate-authority-router.ts | 16 +- .../server/routes/v1/certificate-router.ts | 12 +- .../certificate-authority-service.ts | 17 +- .../certificate-authority-types.ts | 4 +- .../certificate/certificate-body-dal.ts | 10 + .../certificate/certificate-cert-dal.ts | 10 - .../certificate/certificate-service.ts | 8 +- frontend/src/hooks/api/ca/types.ts | 6 +- frontend/src/hooks/api/certificates/types.ts | 1 + .../CertificatesPage/CertificatesPage.tsx | 2 +- .../components/CaTab/components/CaModal.tsx | 26 +- .../components/CaTab/components/CaTable.tsx | 4 +- .../components/CertificateContent.tsx | 231 ++++++++---------- .../components/CertificateModal.tsx | 67 +++-- .../components/CertificatesTable.tsx | 4 +- .../ProjectRoleModifySection.tsx | 16 +- .../ProjectRoleModifySection.utils.ts | 2 + .../SingleProjectPermission.tsx | 2 + 26 files changed, 244 insertions(+), 243 deletions(-) rename backend/src/db/schemas/{certificate-certs.ts => certificate-bodies.ts} (56%) create mode 100644 backend/src/services/certificate/certificate-body-dal.ts delete mode 100644 backend/src/services/certificate/certificate-cert-dal.ts diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index e04ef8a28..e5068ae91 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -44,9 +44,9 @@ import { TCertificateAuthoritySecret, TCertificateAuthoritySecretInsert, TCertificateAuthoritySecretUpdate, - TCertificateCerts, - TCertificateCertsInsert, - TCertificateCertsUpdate, + TCertificateBodies, + TCertificateBodiesInsert, + TCertificateBodiesUpdate, TCertificates, TCertificateSecrets, TCertificateSecretsInsert, @@ -299,10 +299,10 @@ declare module "knex/types/tables" { TCertificateAuthorityCrlUpdate >; [TableName.Certificate]: Knex.CompositeTableType; - [TableName.CertificateCert]: Knex.CompositeTableType< - TCertificateCerts, - TCertificateCertsInsert, - TCertificateCertsUpdate + [TableName.CertificateBody]: Knex.CompositeTableType< + TCertificateBodies, + TCertificateBodiesInsert, + TCertificateBodiesUpdate >; [TableName.CertificateSecret]: Knex.CompositeTableType< TCertificateSecrets, diff --git a/backend/src/db/migrations/20240607032218_certificate-mgmt.ts b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts index 3edac7a72..a738a6b64 100644 --- a/backend/src/db/migrations/20240607032218_certificate-mgmt.ts +++ b/backend/src/db/migrations/20240607032218_certificate-mgmt.ts @@ -24,6 +24,7 @@ export async function up(knex: Knex): Promise { t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); t.string("type").notNullable(); // root / intermediate t.string("status").notNullable(); // active / pending-certificate + t.string("friendlyName").notNullable(); t.string("organization").notNullable(); t.string("ou").notNullable(); t.string("country").notNullable(); @@ -31,7 +32,6 @@ export async function up(knex: Knex): Promise { t.string("locality").notNullable(); t.string("commonName").notNullable(); t.string("dn").notNullable(); - t.unique(["dn", "projectId"]); t.string("serialNumber").nullable().unique(); t.integer("maxPathLength").nullable(); t.string("keyAlgorithm").notNullable(); @@ -80,6 +80,7 @@ export async function up(knex: Knex): Promise { t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); t.string("status").notNullable(); // active / pending-certificate t.string("serialNumber").notNullable().unique(); + t.string("friendlyName").notNullable(); t.string("commonName").notNullable(); t.datetime("notBefore").notNullable(); t.datetime("notAfter").notNullable(); @@ -88,8 +89,8 @@ export async function up(knex: Knex): Promise { }); } - if (!(await knex.schema.hasTable(TableName.CertificateCert))) { - await knex.schema.createTable(TableName.CertificateCert, (t) => { + if (!(await knex.schema.hasTable(TableName.CertificateBody))) { + await knex.schema.createTable(TableName.CertificateBody, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); t.uuid("certId").notNullable().unique(); @@ -102,7 +103,7 @@ export async function up(knex: Knex): Promise { await createOnUpdateTrigger(knex, TableName.CertificateAuthorityCert); await createOnUpdateTrigger(knex, TableName.CertificateAuthoritySecret); await createOnUpdateTrigger(knex, TableName.Certificate); - await createOnUpdateTrigger(knex, TableName.CertificateCert); + await createOnUpdateTrigger(knex, TableName.CertificateBody); } export async function down(knex: Knex): Promise { @@ -115,8 +116,8 @@ export async function down(knex: Knex): Promise { } // certificates - await knex.schema.dropTableIfExists(TableName.CertificateCert); - await dropOnUpdateTrigger(knex, TableName.CertificateCert); + await knex.schema.dropTableIfExists(TableName.CertificateBody); + await dropOnUpdateTrigger(knex, TableName.CertificateBody); await knex.schema.dropTableIfExists(TableName.Certificate); await dropOnUpdateTrigger(knex, TableName.Certificate); diff --git a/backend/src/db/schemas/certificate-authorities.ts b/backend/src/db/schemas/certificate-authorities.ts index 9f0e7f677..16f303b5c 100644 --- a/backend/src/db/schemas/certificate-authorities.ts +++ b/backend/src/db/schemas/certificate-authorities.ts @@ -15,6 +15,7 @@ export const CertificateAuthoritiesSchema = z.object({ projectId: z.string(), type: z.string(), status: z.string(), + friendlyName: z.string(), organization: z.string(), ou: z.string(), country: z.string(), diff --git a/backend/src/db/schemas/certificate-certs.ts b/backend/src/db/schemas/certificate-bodies.ts similarity index 56% rename from backend/src/db/schemas/certificate-certs.ts rename to backend/src/db/schemas/certificate-bodies.ts index b02ef8ab4..75afbddbd 100644 --- a/backend/src/db/schemas/certificate-certs.ts +++ b/backend/src/db/schemas/certificate-bodies.ts @@ -9,7 +9,7 @@ import { zodBuffer } from "@app/lib/zod"; import { TImmutableDBKeys } from "./models"; -export const CertificateCertsSchema = z.object({ +export const CertificateBodiesSchema = z.object({ id: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), @@ -17,6 +17,6 @@ export const CertificateCertsSchema = z.object({ encryptedCertificate: zodBuffer }); -export type TCertificateCerts = z.infer; -export type TCertificateCertsInsert = Omit, TImmutableDBKeys>; -export type TCertificateCertsUpdate = Partial, TImmutableDBKeys>>; +export type TCertificateBodies = z.infer; +export type TCertificateBodiesInsert = Omit, TImmutableDBKeys>; +export type TCertificateBodiesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/certificates.ts b/backend/src/db/schemas/certificates.ts index b487483d0..b635420d5 100644 --- a/backend/src/db/schemas/certificates.ts +++ b/backend/src/db/schemas/certificates.ts @@ -14,6 +14,7 @@ export const CertificatesSchema = z.object({ caId: z.string().uuid(), status: z.string(), serialNumber: z.string(), + friendlyName: z.string(), commonName: z.string(), notBefore: z.date(), notAfter: z.date(), diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index d8c5e4eca..499447018 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -12,7 +12,7 @@ export * from "./certificate-authorities"; export * from "./certificate-authority-certs"; export * from "./certificate-authority-crl"; export * from "./certificate-authority-secret"; -export * from "./certificate-certs"; +export * from "./certificate-bodies"; export * from "./certificate-secrets"; export * from "./certificates"; export * from "./dynamic-secret-leases"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index de478755a..2a0a77f8c 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -7,7 +7,7 @@ export enum TableName { CertificateAuthoritySecret = "certificate_authority_secret", CertificateAuthorityCrl = "certificate_authority_crl", Certificate = "certificates", - CertificateCert = "certificate_certs", + CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", Groups = "groups", GroupProjectMembership = "group_project_memberships", diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9bd64bc0a..30fce0272 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -71,7 +71,7 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service"; import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal"; import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service"; -import { certificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal"; +import { certificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { certificateDALFactory } from "@app/services/certificate/certificate-dal"; import { certificateServiceFactory } from "@app/services/certificate/certificate-service"; import { certificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; @@ -521,11 +521,11 @@ export const registerRoutes = async ( const certificateAuthorityCrlDAL = certificateAuthorityCrlDALFactory(db); const certificateDAL = certificateDALFactory(db); - const certificateCertDAL = certificateCertDALFactory(db); + const certificateBodyDAL = certificateBodyDALFactory(db); const certificateService = certificateServiceFactory({ certificateDAL, - certificateCertDAL, + certificateBodyDAL, certificateAuthorityDAL, certificateAuthorityCertDAL, certificateAuthorityCrlDAL, @@ -552,7 +552,7 @@ export const registerRoutes = async ( certificateAuthorityCrlDAL, certificateAuthorityQueue, certificateDAL, - certificateCertDAL, + certificateBodyDAL, projectDAL, kmsService, permissionService diff --git a/backend/src/server/routes/v1/certificate-authority-router.ts b/backend/src/server/routes/v1/certificate-authority-router.ts index 2520850d3..ff84e86f1 100644 --- a/backend/src/server/routes/v1/certificate-authority-router.ts +++ b/backend/src/server/routes/v1/certificate-authority-router.ts @@ -1,3 +1,4 @@ +import ms from "ms"; import { z } from "zod"; import { CertificateAuthoritiesSchema } from "@app/db/schemas"; @@ -21,7 +22,8 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { body: z .object({ projectSlug: z.string().trim(), - type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]), + type: z.nativeEnum(CaType), + friendlyName: z.string().optional(), commonName: z.string().trim(), organization: z.string().trim(), ou: z.string().trim(), @@ -32,14 +34,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { notBefore: validateCaDateField.optional(), notAfter: validateCaDateField.optional(), maxPathLength: z.number().min(-1).default(-1), - keyAlgorithm: z - .enum([ - CertKeyAlgorithm.RSA_2048, - CertKeyAlgorithm.RSA_4096, - CertKeyAlgorithm.ECDSA_P256, - CertKeyAlgorithm.ECDSA_P384 - ]) - .default(CertKeyAlgorithm.RSA_2048) + keyAlgorithm: z.nativeEnum(CertKeyAlgorithm).default(CertKeyAlgorithm.RSA_2048) }) .refine( (data) => { @@ -342,8 +337,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => { }), body: z .object({ + friendlyName: z.string().optional(), commonName: z.string().trim().min(1), - ttl: z.number().int().min(0).optional(), + ttl: z.string().refine((val) => ms(val) > 0, "TTL must be a positive number"), notBefore: validateCaDateField.optional(), notAfter: validateCaDateField.optional() }) diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 408cd4d64..257edec1f 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -52,17 +52,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => { serialNumber: z.string().trim() }), body: z.object({ - revocationReason: z.enum([ - CrlReason.UNSPECIFIED, - CrlReason.KEY_COMPROMISE, - CrlReason.CA_COMPROMISE, - CrlReason.AFFILIATION_CHANGED, - CrlReason.SUPERSEDED, - CrlReason.CESSATION_OF_OPERATION, - CrlReason.CERTIFICATE_HOLD, - CrlReason.PRIVILEGE_WITHDRAWN, - CrlReason.A_A_COMPROMISE - ]) + revocationReason: z.nativeEnum(CrlReason) }), response: { 200: z.object({ diff --git a/backend/src/services/certificate-authority/certificate-authority-service.ts b/backend/src/services/certificate-authority/certificate-authority-service.ts index 32839cccc..a97e2df7e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-service.ts +++ b/backend/src/services/certificate-authority/certificate-authority-service.ts @@ -2,11 +2,12 @@ import { ForbiddenError } from "@casl/ability"; import * as x509 from "@peculiar/x509"; import crypto, { KeyObject } from "crypto"; +import ms from "ms"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; -import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TProjectDALFactory } from "@app/services/project/project-dal"; @@ -50,7 +51,7 @@ type TCertificateAuthorityServiceFactoryDep = { certificateAuthorityCrlDAL: Pick; certificateAuthorityQueue: TCertificateAuthorityQueueFactory; // TODO: Pick certificateDAL: Pick; - certificateCertDAL: Pick; + certificateBodyDAL: Pick; projectDAL: Pick; kmsService: Pick; permissionService: Pick; @@ -64,7 +65,7 @@ export const certificateAuthorityServiceFactory = ({ certificateAuthoritySecretDAL, certificateAuthorityCrlDAL, certificateDAL, - certificateCertDAL, + certificateBodyDAL, projectDAL, kmsService, permissionService @@ -75,6 +76,7 @@ export const certificateAuthorityServiceFactory = ({ const createCa = async ({ projectSlug, type, + friendlyName, commonName, organization, ou, @@ -127,6 +129,7 @@ export const certificateAuthorityServiceFactory = ({ : new Date(new Date().setFullYear(new Date().getFullYear() + 10)); const serialNumber = crypto.randomBytes(32).toString("hex"); + const ca = await certificateAuthorityDAL.create( { projectId: project.id, @@ -136,6 +139,7 @@ export const certificateAuthorityServiceFactory = ({ country, province, locality, + friendlyName: friendlyName || dn, commonName, status: type === CaType.ROOT ? CaStatus.ACTIVE : CaStatus.PENDING_CERTIFICATE, dn, @@ -642,6 +646,7 @@ export const certificateAuthorityServiceFactory = ({ */ const issueCertFromCa = async ({ caId, + friendlyName, commonName, ttl, notBefore, @@ -688,8 +693,7 @@ export const certificateAuthorityServiceFactory = ({ if (notAfter) { notAfterDate = new Date(notAfter); } else if (ttl) { - // ttl in seconds - notAfterDate = new Date(new Date().getTime() + ttl * 1000); + notAfterDate = new Date(new Date().getTime() + ms(ttl)); } const caCertNotBeforeDate = new Date(caCertObj.notBefore); @@ -760,6 +764,7 @@ export const certificateAuthorityServiceFactory = ({ { caId: ca.id, status: CertStatus.ACTIVE, + friendlyName: friendlyName || commonName, commonName, serialNumber, notBefore: notBeforeDate, @@ -768,7 +773,7 @@ export const certificateAuthorityServiceFactory = ({ tx ); - await certificateCertDAL.create( + await certificateBodyDAL.create( { certId: cert.id, encryptedCertificate diff --git a/backend/src/services/certificate-authority/certificate-authority-types.ts b/backend/src/services/certificate-authority/certificate-authority-types.ts index d9fa0f59d..d6e17159e 100644 --- a/backend/src/services/certificate-authority/certificate-authority-types.ts +++ b/backend/src/services/certificate-authority/certificate-authority-types.ts @@ -23,6 +23,7 @@ export enum CaStatus { export type TCreateCaDTO = { projectSlug: string; type: CaType; + friendlyName?: string; commonName: string; organization: string; ou: string; @@ -72,8 +73,9 @@ export type TImportCertToCaDTO = { export type TIssueCertFromCaDTO = { caId: string; + friendlyName?: string; commonName: string; - ttl?: number; + ttl: string; notBefore?: string; notAfter?: string; } & Omit; diff --git a/backend/src/services/certificate/certificate-body-dal.ts b/backend/src/services/certificate/certificate-body-dal.ts new file mode 100644 index 000000000..9ddc98966 --- /dev/null +++ b/backend/src/services/certificate/certificate-body-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TCertificateBodyDALFactory = ReturnType; + +export const certificateBodyDALFactory = (db: TDbClient) => { + const certificateBodyOrm = ormify(db, TableName.CertificateBody); + return certificateBodyOrm; +}; diff --git a/backend/src/services/certificate/certificate-cert-dal.ts b/backend/src/services/certificate/certificate-cert-dal.ts deleted file mode 100644 index b2dd0001b..000000000 --- a/backend/src/services/certificate/certificate-cert-dal.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TCertificateCertDALFactory = ReturnType; - -export const certificateCertDALFactory = (db: TDbClient) => { - const certificateCertOrm = ormify(db, TableName.CertificateCert); - return certificateCertOrm; -}; diff --git a/backend/src/services/certificate/certificate-service.ts b/backend/src/services/certificate/certificate-service.ts index 3e9dc5160..b5a44b748 100644 --- a/backend/src/services/certificate/certificate-service.ts +++ b/backend/src/services/certificate/certificate-service.ts @@ -3,7 +3,7 @@ import * as x509 from "@peculiar/x509"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { TCertificateCertDALFactory } from "@app/services/certificate/certificate-cert-dal"; +import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/services/certificate-authority/certificate-authority-crl-dal"; @@ -19,7 +19,7 @@ import { CertStatus, TDeleteCertDTO, TGetCertCertDTO, TGetCertDTO, TRevokeCertDT type TCertificateServiceFactoryDep = { certificateDAL: Pick; - certificateCertDAL: Pick; + certificateBodyDAL: Pick; certificateAuthorityDAL: Pick; certificateAuthorityCertDAL: Pick; certificateAuthorityCrlDAL: Pick; @@ -33,7 +33,7 @@ export type TCertificateServiceFactory = ReturnType
-

Certificates

+

Internal PKI

Certificates diff --git a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx index 353211c99..28fd1adbe 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CaTab/components/CaModal.tsx @@ -35,6 +35,7 @@ const getDateTenYearsFromToday = () => { const schema = z .object({ type: z.enum([CaType.ROOT, CaType.INTERMEDIATE]), + friendlyName: z.string(), organization: z.string(), ou: z.string(), country: z.string(), @@ -81,6 +82,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { resolver: zodResolver(schema), defaultValues: { type: CaType.ROOT, + friendlyName: "", organization: "", ou: "", country: "", @@ -99,6 +101,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { if (ca) { reset({ type: ca.type, + friendlyName: ca.friendlyName, organization: ca.organization, ou: ca.ou, country: ca.country, @@ -112,6 +115,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { } else { reset({ type: CaType.ROOT, + friendlyName: "", organization: "", ou: "", country: "", @@ -127,6 +131,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { const onFormSubmit = async ({ type, + friendlyName, commonName, organization, ou, @@ -143,6 +148,7 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { await createMutateAsync({ projectSlug: currentWorkspace.slug, type, + friendlyName, commonName, organization, ou, @@ -170,12 +176,6 @@ export const CaModal = ({ popUp, handlePopUpToggle }: Props) => { } }; - // const getDefaultNotAfterDate = () => { - // const date = new Date(); - // date.setFullYear(date.getFullYear() + 10); - // return date; - // }; - return ( { )} /> + ( + + + + )} + /> { - + @@ -71,7 +71,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => { data.map((ca) => { return ( - + diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx index 42746d91a..8c40af74f 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateContent.tsx @@ -1,9 +1,9 @@ -import { useEffect } from "react"; import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import FileSaver from "file-saver"; -import { IconButton } from "@app/components/v2"; -import { useToggle } from "@app/hooks"; +import { IconButton, Tooltip } from "@app/components/v2"; +import { useTimedReset } from "@app/hooks"; type Props = { serialNumber: string; @@ -18,42 +18,28 @@ export const CertificateContent = ({ certificateChain, privateKey }: Props) => { - const [isSerialNumberCopied, setIsSerialNumberCopied] = useToggle(false); - const [isCertificateCopied, setIsCertificateCopied] = useToggle(false); - const [isCertificateChainCopied, setIsCertificateChainCopied] = useToggle(false); - const [isCertificateSkCopied, setIsCertificateSkCopied] = useToggle(false); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isSerialNumberCopied) { - timer = setTimeout(() => setIsSerialNumberCopied.off(), 2000); + const [copyTextSerialNumber, isCopyingSerialNumber, setCopyTextSerialNumber] = + useTimedReset({ + initialState: "Copy to clipboard" + }); + const [copyTextCertificate, isCopyingCertificate, setCopyTextCertificate] = useTimedReset( + { + initialState: "Copy to clipboard" } + ); + const [copyTextCertificateChain, isCopyingCertificateChain, setCopyTextCertificateChain] = + useTimedReset({ + initialState: "Copy to clipboard" + }); - if (isCertificateCopied) { - timer = setTimeout(() => setIsCertificateCopied.off(), 2000); - } - - if (isCertificateChainCopied) { - timer = setTimeout(() => setIsCertificateChainCopied.off(), 2000); - } - - if (isCertificateSkCopied) { - timer = setTimeout(() => setIsCertificateSkCopied.off(), 2000); - } - - return () => clearTimeout(timer); - }, [isSerialNumberCopied, isCertificateCopied, isCertificateChainCopied, isCertificateSkCopied]); + const [copyTextCertificateSk, isCopyingCertificateSk, setCopyTextCertificateSk] = + useTimedReset({ + initialState: "Copy to clipboard" + }); const downloadTxtFile = (filename: string, content: string) => { - const blob = new Blob([content], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = filename; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + const blob = new Blob([content], { type: "text/plain;charset=utf-8" }); + FileSaver.saveAs(blob, filename); }; return ( @@ -61,51 +47,48 @@ export const CertificateContent = ({

Serial Number

{serialNumber}

- { - navigator.clipboard.writeText(serialNumber); - setIsSerialNumberCopied.on(); - }} - > - - - Click to copy - - -
-
-

Certificate Body

-
+ { - navigator.clipboard.writeText(certificate); - setIsCertificateCopied.on(); + navigator.clipboard.writeText(serialNumber); + setCopyTextSerialNumber("Copied"); }} > - - - Copy - - - { - downloadTxtFile("cert.pem", certificate); - }} - > - - - Download - + + +
+
+

Certificate Body

+
+ + { + navigator.clipboard.writeText(certificate); + setCopyTextCertificate("Copied"); + }} + > + + + + + { + downloadTxtFile("cert.pem", certificate); + }} + > + + +
@@ -116,33 +99,31 @@ export const CertificateContent = ({

Certificate Chain

- { - navigator.clipboard.writeText(certificateChain); - setIsCertificateChainCopied.on(); - }} - > - - - Copy - - - { - downloadTxtFile("chain.pem", certificateChain); - }} - > - - - Download - - + + { + navigator.clipboard.writeText(certificateChain); + setCopyTextCertificateChain("Copied"); + }} + > + + + + + { + downloadTxtFile("chain.pem", certificateChain); + }} + > + + +
@@ -155,33 +136,31 @@ export const CertificateContent = ({

Certificate Private Key

- { - navigator.clipboard.writeText(privateKey); - setIsCertificateSkCopied.on(); - }} - > - - - Copy - - - { - downloadTxtFile("private_key.txt", privateKey); - }} - > - - - Download - - + + { + navigator.clipboard.writeText(privateKey); + setCopyTextCertificateSk("Copied"); + }} + > + + + + + { + downloadTxtFile("private_key.txt", privateKey); + }} + > + + +
diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx index 99756f8c5..03e70a80c 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificateModal.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { format } from "date-fns"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -21,21 +20,11 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; import { CertificateContent } from "./CertificateContent"; -const isValidDate = (dateString: string) => { - if (dateString === "") return true; - const date = new Date(dateString); - return !Number.isNaN(date.getTime()); -}; - const schema = z.object({ caId: z.string(), + friendlyName: z.string(), commonName: z.string().trim().min(1), - ttl: z.string().trim().optional(), - notAfter: z - .string() - .trim() - .refine(isValidDate, { message: "Invalid date format" }) - .transform((val) => (val === "" ? undefined : val)) + ttl: z.string().trim() }); export type FormData = z.infer; @@ -80,31 +69,30 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { if (cert) { reset({ caId: cert.caId, + friendlyName: cert.friendlyName, commonName: cert.commonName, - ttl: "", - notAfter: format(new Date(cert.notAfter), "yyyy-MM-dd") + ttl: "" }); } else { reset({ caId: "", + friendlyName: "", commonName: "", - ttl: "", - notAfter: "" + ttl: "" }); } }, [cert]); - const onFormSubmit = async ({ caId, commonName, ttl, notAfter }: FormData) => { + const onFormSubmit = async ({ caId, friendlyName, commonName, ttl }: FormData) => { try { if (!currentWorkspace?.slug) return; const { serialNumber, certificate, certificateChain, privateKey } = await createCertificate({ projectSlug: currentWorkspace.slug, caId, + friendlyName, commonName, - ttl: ttl ? Number(ttl) : undefined, - notBefore: new Date().toISOString(), - notAfter + ttl }); reset(); @@ -175,6 +163,20 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => { )} /> + ( + + + + )} + /> { name="ttl" render={({ field, fieldState: { error } }) => ( - - - )} - /> - ( - - + )} /> diff --git a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx index db02f2386..371c432a1 100644 --- a/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/CertificatesTab/components/CertificatesTable.tsx @@ -53,7 +53,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
SubjectFriendly Name Status Type Valid Until
{ca.dn}{ca.friendlyName} {caStatusToNameMap[ca.status]} {caTypeToNameMap[ca.type]} {ca.notAfter ? format(new Date(ca.notAfter), "yyyy-MM-dd") : "-"}
- + - +
Common NameFriendly Name Status Valid Until @@ -67,7 +67,7 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => { data.map((certificate) => { return (
{certificate.commonName}{certificate.friendlyName} {certStatusToNameMap[certificate.status]} {certificate.notAfter diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index cd2eaeee0..2140887de 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -4,6 +4,7 @@ import { faAnchorLock, faArrowLeft, faBook, + faCertificate, faCog, faKey, faLock, @@ -13,8 +14,7 @@ import { faShield, faTags, faUser, - faUsers -} from "@fortawesome/free-solid-svg-icons"; + faUsers} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -117,6 +117,18 @@ const SINGLE_PERMISSION_LIST = [ subtitle: "IP allowlist management control", icon: faNetworkWired, formName: "ip-allowlist" + }, + { + title: "Certificate Authorities", + subtitle: "CA management control", + icon: faCertificate, + formName: "certificate-authorities" + }, + { + title: "Certificates", + subtitle: "Certificate management control", + icon: faCertificate, + formName: "certificates" } ] as const; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts index 4d4d45e64..0b534347d 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.utils.ts @@ -48,6 +48,8 @@ export const formSchema = z.object({ tags: generalPermissionSchema, "audit-logs": generalPermissionSchema, "ip-allowlist": generalPermissionSchema, + "certificate-authorities": generalPermissionSchema, + certificates: generalPermissionSchema, // akhilmhdh: refactor all keys like below [ProjectPermissionSub.SecretApproval]: generalPermissionSchema, workspace: z diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx index fd65cfd58..adfae81b1 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/SingleProjectPermission.tsx @@ -25,6 +25,8 @@ type Props = { | "audit-logs" | "ip-allowlist" | "identity" + | "certificate-authorities" + | "certificates" | ProjectPermissionSub.SecretApproval; isNonEditable?: boolean; setValue: UseFormSetValue;