From fc4a20caf24cb2318189000d30426ae1681e149f Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Fri, 9 Aug 2024 08:27:53 -0700 Subject: [PATCH] Rename pki alerting structures --- backend/src/@types/fastify.d.ts | 4 +- backend/src/@types/knex.d.ts | 16 +- .../20240806173521_cert-alerting.ts | 29 ++- backend/src/db/schemas/dynamic-secrets.ts | 10 +- backend/src/db/schemas/index.ts | 3 +- backend/src/db/schemas/models.ts | 3 +- .../db/schemas/{alerts.ts => pki-alerts.ts} | 8 +- .../src/db/schemas/pki-collection-items.ts | 21 ++ backend/src/db/schemas/webhooks.ts | 15 +- backend/src/server/routes/index.ts | 19 +- .../src/server/routes/v1/pki-alert-router.ts | 18 +- .../server/routes/v1/pki-collection-router.ts | 164 ++++++++++++++- .../src/server/routes/v2/project-router.ts | 4 +- backend/src/services/alert/alert-dal.ts | 12 -- .../src/services/pki-alert/pki-alert-dal.ts | 12 ++ .../pki-alert-service.ts} | 28 +-- .../pki-alert-types.ts} | 0 .../pki-collection/pki-collection-fns.ts | 30 +++ .../pki-collection/pki-collection-item-dal.ts | 33 +++ .../pki-collection/pki-collection-service.ts | 179 +++++++++++++++- .../pki-collection/pki-collection-types.ts | 22 ++ .../src/services/project/project-service.ts | 10 +- .../context/ProjectPermissionContext/types.ts | 3 +- frontend/src/hooks/api/pkiAlerts/index.tsx | 4 +- .../src/hooks/api/pkiAlerts/mutations.tsx | 6 +- frontend/src/hooks/api/pkiAlerts/queries.tsx | 2 +- .../hooks/api/pkiCollections/constants.tsx | 9 + .../src/hooks/api/pkiCollections/index.tsx | 4 +- .../hooks/api/pkiCollections/mutations.tsx | 39 +++- .../src/hooks/api/pkiCollections/queries.tsx | 56 ++++- .../src/hooks/api/pkiCollections/types.ts | 20 ++ frontend/src/hooks/api/workspace/index.tsx | 2 +- frontend/src/hooks/api/workspace/queries.tsx | 5 +- .../pki-collections/[collectionId]/index.tsx | 20 ++ .../CertificatesPage/CertificatesPage.tsx | 4 +- .../components/AlertsTab/index.tsx | 1 - .../PkiAlertsTab.tsx} | 6 +- .../components/PkiAlertModal.tsx} | 42 ++-- .../components/PkiAlertRow.tsx} | 13 +- .../components/PkiAlertsSection.tsx} | 34 +-- .../components/PkiAlertsTable.tsx} | 21 +- .../components/PkiCollectionModal.tsx | 10 +- .../components/PkiCollectionSection.tsx | 2 +- .../components/PkiCollectionTable.tsx | 16 +- .../components/index.tsx | 2 +- .../components/PkiAlertsTab/index.tsx | 1 + .../CertificatesPage/components/index.tsx | 2 +- .../PkiCollectionPage/PkiCollectionPage.tsx | 160 ++++++++++++++ .../components/AddPkiCollectionItemModal.tsx | 199 ++++++++++++++++++ .../PkiCollectionDetailsSection.tsx | 83 ++++++++ .../components/PkiCollectionItemsSection.tsx | 83 ++++++++ .../components/PkiCollectionItemsTable.tsx | 112 ++++++++++ .../PkiCollectionPage/components/index.tsx | 2 + .../views/Project/PkiCollectionPage/index.tsx | 1 + 54 files changed, 1443 insertions(+), 161 deletions(-) rename backend/src/db/schemas/{alerts.ts => pki-alerts.ts} (62%) create mode 100644 backend/src/db/schemas/pki-collection-items.ts delete mode 100644 backend/src/services/alert/alert-dal.ts create mode 100644 backend/src/services/pki-alert/pki-alert-dal.ts rename backend/src/services/{alert/alert-service.ts => pki-alert/pki-alert-service.ts} (84%) rename backend/src/services/{alert/alert-types.ts => pki-alert/pki-alert-types.ts} (100%) create mode 100644 backend/src/services/pki-collection/pki-collection-fns.ts create mode 100644 backend/src/services/pki-collection/pki-collection-item-dal.ts create mode 100644 frontend/src/hooks/api/pkiCollections/constants.tsx create mode 100644 frontend/src/pages/project/[id]/pki-collections/[collectionId]/index.tsx delete mode 100644 frontend/src/views/Project/CertificatesPage/components/AlertsTab/index.tsx rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab/AlertsTab.tsx => PkiAlertsTab/PkiAlertsTab.tsx} (71%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab/components/AlertModal.tsx => PkiAlertsTab/components/PkiAlertModal.tsx} (88%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab/components/AlertRow.tsx => PkiAlertsTab/components/PkiAlertRow.tsx} (89%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab/components/AlertsSection.tsx => PkiAlertsTab/components/PkiAlertsSection.tsx} (69%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab/components/AlertsTable.tsx => PkiAlertsTab/components/PkiAlertsTable.tsx} (67%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab => PkiAlertsTab}/components/PkiCollectionModal.tsx (92%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab => PkiAlertsTab}/components/PkiCollectionSection.tsx (99%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab => PkiAlertsTab}/components/PkiCollectionTable.tsx (87%) rename frontend/src/views/Project/CertificatesPage/components/{AlertsTab => PkiAlertsTab}/components/index.tsx (53%) create mode 100644 frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/index.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/PkiCollectionPage.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/components/AddPkiCollectionItemModal.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionDetailsSection.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsSection.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/components/index.tsx create mode 100644 frontend/src/views/Project/PkiCollectionPage/index.tsx diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index dab2e1996..bd9e483ae 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -27,7 +27,6 @@ import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/ import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service"; import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service"; import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; -import { TAlertServiceFactory } from "@app/services/alert/alert-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; @@ -52,6 +51,7 @@ import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/i import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service"; import { TOrgServiceFactory } from "@app/services/org/org-service"; import { TOrgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; +import { TPkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { TPkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { TProjectServiceFactory } from "@app/services/project/project-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; @@ -116,7 +116,7 @@ declare module "fastify" { group: TGroupServiceFactory; groupProject: TGroupProjectServiceFactory; apiKey: TApiKeyServiceFactory; - alert: TAlertServiceFactory; + pkiAlert: TPkiAlertServiceFactory; project: TProjectServiceFactory; projectMembership: TProjectMembershipServiceFactory; projectEnv: TProjectEnvServiceFactory; diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 754c36de2..0dc781092 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -14,9 +14,6 @@ import { TAccessApprovalRequestsReviewersInsert, TAccessApprovalRequestsReviewersUpdate, TAccessApprovalRequestsUpdate, - TAlerts, - TAlertsInsert, - TAlertsUpdate, TApiKeys, TApiKeysInsert, TApiKeysUpdate, @@ -164,6 +161,12 @@ import { TOrgRoles, TOrgRolesInsert, TOrgRolesUpdate, + TPkiAlerts, + TPkiAlertsInsert, + TPkiAlertsUpdate, + TPkiCollectionItems, + TPkiCollectionItemsInsert, + TPkiCollectionItemsUpdate, TPkiCollections, TPkiCollectionsInsert, TPkiCollectionsUpdate, @@ -371,11 +374,17 @@ declare module "knex/types/tables" { TCertificateSecretsInsert, TCertificateSecretsUpdate >; + [TableName.PkiAlert]: KnexOriginal.CompositeTableType; [TableName.PkiCollection]: KnexOriginal.CompositeTableType< TPkiCollections, TPkiCollectionsInsert, TPkiCollectionsUpdate >; + [TableName.PkiCollectionItem]: KnexOriginal.CompositeTableType< + TPkiCollectionItems, + TPkiCollectionItemsInsert, + TPkiCollectionItemsUpdate + >; [TableName.UserGroupMembership]: KnexOriginal.CompositeTableType< TUserGroupMembership, TUserGroupMembershipInsert, @@ -427,7 +436,6 @@ declare module "knex/types/tables" { [TableName.UserAction]: KnexOriginal.CompositeTableType; [TableName.SuperAdmin]: KnexOriginal.CompositeTableType; [TableName.ApiKey]: KnexOriginal.CompositeTableType; - [TableName.Alert]: KnexOriginal.CompositeTableType; [TableName.Project]: KnexOriginal.CompositeTableType; [TableName.ProjectMembership]: KnexOriginal.CompositeTableType< TProjectMemberships, diff --git a/backend/src/db/migrations/20240806173521_cert-alerting.ts b/backend/src/db/migrations/20240806173521_cert-alerting.ts index 1cc94badb..a04955dc0 100644 --- a/backend/src/db/migrations/20240806173521_cert-alerting.ts +++ b/backend/src/db/migrations/20240806173521_cert-alerting.ts @@ -14,9 +14,21 @@ export async function up(knex: Knex): Promise { }); } - if (!(await knex.schema.hasTable(TableName.Alert))) { - // TODO: rename to pki alert - await knex.schema.createTable(TableName.Alert, (t) => { + if (!(await knex.schema.hasTable(TableName.PkiCollectionItem))) { + await knex.schema.createTable(TableName.PkiCollectionItem, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.timestamps(true, true, true); + t.uuid("pkiCollectionId").notNullable(); + t.foreign("pkiCollectionId").references("id").inTable(TableName.PkiCollection).onDelete("CASCADE"); + t.uuid("caId").nullable(); + t.foreign("caId").references("id").inTable(TableName.CertificateAuthority).onDelete("CASCADE"); + t.uuid("certId").nullable(); + t.foreign("certId").references("id").inTable(TableName.Certificate).onDelete("CASCADE"); + }); + } + + if (!(await knex.schema.hasTable(TableName.PkiAlert))) { + await knex.schema.createTable(TableName.PkiAlert, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.timestamps(true, true, true); t.string("projectId").notNullable(); @@ -30,13 +42,16 @@ export async function up(knex: Knex): Promise { } await createOnUpdateTrigger(knex, TableName.PkiCollection); - await createOnUpdateTrigger(knex, TableName.Alert); + await createOnUpdateTrigger(knex, TableName.PkiAlert); } export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.PkiAlert); + await dropOnUpdateTrigger(knex, TableName.PkiAlert); + + await knex.schema.dropTableIfExists(TableName.PkiCollectionItem); + await dropOnUpdateTrigger(knex, TableName.PkiCollectionItem); + await knex.schema.dropTableIfExists(TableName.PkiCollection); await dropOnUpdateTrigger(knex, TableName.PkiCollection); - - await knex.schema.dropTableIfExists(TableName.Alert); - await dropOnUpdateTrigger(knex, TableName.Alert); } diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index d90f1f7d2..b27da396c 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), + inputIV: z.string(), + inputCiphertext: z.string(), + inputTag: z.string(), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date(), - encryptedConfig: zodBuffer + updatedAt: z.date() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 98af39fcd..03146f204 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -2,7 +2,6 @@ export * from "./access-approval-policies"; export * from "./access-approval-policies-approvers"; export * from "./access-approval-requests"; export * from "./access-approval-requests-reviewers"; -export * from "./alerts"; export * from "./api-keys"; export * from "./audit-log-streams"; export * from "./audit-logs"; @@ -53,6 +52,8 @@ export * from "./org-bots"; export * from "./org-memberships"; export * from "./org-roles"; export * from "./organizations"; +export * from "./pki-alerts"; +export * from "./pki-collection-items"; export * from "./pki-collections"; export * from "./project-bots"; export * from "./project-environments"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 0087b33e0..ebbead70c 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -9,8 +9,9 @@ export enum TableName { Certificate = "certificates", CertificateBody = "certificate_bodies", CertificateSecret = "certificate_secrets", - Alert = "alerts", // TODO: rename + PkiAlert = "pki_alerts", PkiCollection = "pki_collections", + PkiCollectionItem = "pki_collection_items", Groups = "groups", GroupProjectMembership = "group_project_memberships", GroupProjectMembershipRole = "group_project_membership_roles", diff --git a/backend/src/db/schemas/alerts.ts b/backend/src/db/schemas/pki-alerts.ts similarity index 62% rename from backend/src/db/schemas/alerts.ts rename to backend/src/db/schemas/pki-alerts.ts index 344bb499d..7bc9d2d7c 100644 --- a/backend/src/db/schemas/alerts.ts +++ b/backend/src/db/schemas/pki-alerts.ts @@ -7,7 +7,7 @@ import { z } from "zod"; import { TImmutableDBKeys } from "./models"; -export const AlertsSchema = z.object({ +export const PkiAlertsSchema = z.object({ id: z.string().uuid(), createdAt: z.date(), updatedAt: z.date(), @@ -18,6 +18,6 @@ export const AlertsSchema = z.object({ recipientEmails: z.string() }); -export type TAlerts = z.infer; -export type TAlertsInsert = Omit, TImmutableDBKeys>; -export type TAlertsUpdate = Partial, TImmutableDBKeys>>; +export type TPkiAlerts = z.infer; +export type TPkiAlertsInsert = Omit, TImmutableDBKeys>; +export type TPkiAlertsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/pki-collection-items.ts b/backend/src/db/schemas/pki-collection-items.ts new file mode 100644 index 000000000..f04f5a1ee --- /dev/null +++ b/backend/src/db/schemas/pki-collection-items.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const PkiCollectionItemsSchema = z.object({ + id: z.string().uuid(), + createdAt: z.date(), + updatedAt: z.date(), + pkiCollectionId: z.string().uuid(), + caId: z.string().uuid().nullable().optional(), + certId: z.string().uuid().nullable().optional() +}); + +export type TPkiCollectionItems = z.infer; +export type TPkiCollectionItemsInsert = Omit, TImmutableDBKeys>; +export type TPkiCollectionItemsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 3f670497f..a7aac2933 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,22 +5,27 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), + url: z.string(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), + encryptedSecretKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + algorithm: z.string().nullable().optional(), + keyEncoding: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), envId: z.string().uuid(), - type: z.string().default("general").nullable().optional(), - encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), - encryptedUrl: zodBuffer + urlCipherText: z.string().nullable().optional(), + urlIV: z.string().nullable().optional(), + urlTag: z.string().nullable().optional(), + type: z.string().default("general").nullable().optional() }); export type TWebhooks = z.infer; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index b1827ea66..279df0bd1 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -73,8 +73,6 @@ import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { TQueueServiceFactory } from "@app/queue"; import { readLimit } from "@app/server/config/rateLimiter"; -import { alertDALFactory } from "@app/services/alert/alert-dal"; -import { alertServiceFactory } from "@app/services/alert/alert-service"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { authDALFactory } from "@app/services/auth/auth-dal"; @@ -133,7 +131,10 @@ import { orgRoleServiceFactory } from "@app/services/org/org-role-service"; import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; +import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; +import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; import { pkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; +import { pkiCollectionItemDALFactory } from "@app/services/pki-collection/pki-collection-item-dal"; import { pkiCollectionServiceFactory } from "@app/services/pki-collection/pki-collection-service"; import { projectDALFactory } from "@app/services/project/project-dal"; import { projectQueueFactory } from "@app/services/project/project-queue"; @@ -220,7 +221,6 @@ export const registerRoutes = async ( const superAdminDAL = superAdminDALFactory(db); const rateLimitDAL = rateLimitDALFactory(db); const apiKeyDAL = apiKeyDALFactory(db); - const alertDAL = alertDALFactory(db); const projectDAL = projectDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db); @@ -588,7 +588,9 @@ export const registerRoutes = async ( const certificateDAL = certificateDALFactory(db); const certificateBodyDAL = certificateBodyDALFactory(db); + const pkiAlertDAL = pkiAlertDALFactory(db); const pkiCollectionDAL = pkiCollectionDALFactory(db); + const pkiCollectionItemDAL = pkiCollectionItemDALFactory(db); const certificateService = certificateServiceFactory({ certificateDAL, @@ -634,14 +636,17 @@ export const registerRoutes = async ( licenseService }); - const alertService = alertServiceFactory({ - alertDAL, + const pkiAlertService = pkiAlertServiceFactory({ + pkiAlertDAL, pkiCollectionDAL, permissionService }); const pkiCollectionService = pkiCollectionServiceFactory({ pkiCollectionDAL, + pkiCollectionItemDAL, + certificateAuthorityDAL, + certificateDAL, permissionService }); @@ -661,7 +666,7 @@ export const registerRoutes = async ( licenseService, certificateAuthorityDAL, certificateDAL, - alertDAL, + pkiAlertDAL, pkiCollectionDAL, projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, @@ -1091,7 +1096,6 @@ export const registerRoutes = async ( orgRole: orgRoleService, oidc: oidcService, apiKey: apiKeyService, - alert: alertService, authToken: tokenService, superAdmin: superAdminService, project: projectService, @@ -1135,6 +1139,7 @@ export const registerRoutes = async ( certificate: certificateService, certificateAuthority: certificateAuthorityService, certificateAuthorityCrl: certificateAuthorityCrlService, + pkiAlert: pkiAlertService, pkiCollection: pkiCollectionService, secretScanning: secretScanningService, license: licenseService, diff --git a/backend/src/server/routes/v1/pki-alert-router.ts b/backend/src/server/routes/v1/pki-alert-router.ts index a330f8c20..902807390 100644 --- a/backend/src/server/routes/v1/pki-alert-router.ts +++ b/backend/src/server/routes/v1/pki-alert-router.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { AlertsSchema } from "@app/db/schemas"; +import { PkiAlertsSchema } from "@app/db/schemas"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -23,11 +23,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { emails: z.array(z.string()) }), response: { - 200: AlertsSchema + 200: PkiAlertsSchema } }, handler: async (req) => { - const alert = await server.services.alert.createPkiAlert({ + const alert = await server.services.pkiAlert.createPkiAlert({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, @@ -67,11 +67,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { alertId: z.string().trim() }), response: { - 200: AlertsSchema + 200: PkiAlertsSchema } }, handler: async (req) => { - const alert = await server.services.alert.getPkiAlertById({ + const alert = await server.services.pkiAlert.getPkiAlertById({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -116,11 +116,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { emails: z.array(z.string()).optional() }), response: { - 200: AlertsSchema + 200: PkiAlertsSchema } }, handler: async (req) => { - const alert = await server.services.alert.updatePkiAlert({ + const alert = await server.services.pkiAlert.updatePkiAlert({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, @@ -160,11 +160,11 @@ export const registerPkiAlertRouter = async (server: FastifyZodProvider) => { alertId: z.string().trim() }), response: { - 200: AlertsSchema + 200: PkiAlertsSchema } }, handler: async (req) => { - const alert = await server.services.alert.deletePkiAlert({ + const alert = await server.services.pkiAlert.deletePkiAlert({ alertId: req.params.alertId, actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/server/routes/v1/pki-collection-router.ts b/backend/src/server/routes/v1/pki-collection-router.ts index a41116a29..23a63497f 100644 --- a/backend/src/server/routes/v1/pki-collection-router.ts +++ b/backend/src/server/routes/v1/pki-collection-router.ts @@ -1,9 +1,10 @@ import { z } from "zod"; -import { PkiCollectionsSchema } from "@app/db/schemas"; +import { PkiCollectionItemsSchema, PkiCollectionsSchema } from "@app/db/schemas"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; +import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => { server.route({ @@ -184,4 +185,165 @@ export const registerPkiCollectionRouter = async (server: FastifyZodProvider) => return pkiCollection; } }); + + server.route({ + method: "GET", + url: "/:collectionId/items", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Get items in PKI collection", + params: z.object({ + collectionId: z.string().trim() + }), + querystring: z.object({ + offset: z.coerce.number().min(0).max(100).default(0), + limit: z.coerce.number().min(1).max(100).default(25) + }), + response: { + 200: z.object({ + collectionItems: z.array( + PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ + type: z.nativeEnum(PkiItemType), + itemId: z.string().trim() + }) + ), + totalCount: z.number() + }) + } + }, + handler: async (req) => { + const { pkiCollectionItems, totalCount } = await server.services.pkiCollection.getPkiCollectionItems({ + collectionId: req.params.collectionId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + + // TODO: audit logging + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: ca.projectId, + // event: { + // type: EventType.REVOKE_CERT, + // metadata: { + // certId: cert.id, + // cn: cert.commonName, + // serialNumber: cert.serialNumber + // } + // } + // }); + + return { + collectionItems: pkiCollectionItems, + totalCount + }; + } + }); + + server.route({ + method: "POST", + url: "/:collectionId/items", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Add item to PKI collection", + params: z.object({ + collectionId: z.string().trim() + }), + body: z.object({ + type: z.nativeEnum(PkiItemType), + itemId: z.string().trim() + }), + response: { + 200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ + type: z.nativeEnum(PkiItemType), + itemId: z.string().trim() + }) + } + }, + handler: async (req) => { + const pkiCollectionItem = await server.services.pkiCollection.addItemToPkiCollection({ + collectionId: req.params.collectionId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.body + }); + + // TODO: audit logging + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: ca.projectId, + // event: { + // type: EventType.REVOKE_CERT, + // metadata: { + // certId: cert.id, + // cn: cert.commonName, + // serialNumber: cert.serialNumber + // } + // } + // }); + + return pkiCollectionItem; + } + }); + + server.route({ + method: "DELETE", + url: "/:collectionId/items/:itemId", + config: { + rateLimit: writeLimit + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + schema: { + description: "Remove item from PKI collection", + params: z.object({ + collectionId: z.string().trim(), + itemId: z.string().trim() + }), + response: { + 200: PkiCollectionItemsSchema.omit({ caId: true, certId: true }).extend({ + type: z.nativeEnum(PkiItemType), + itemId: z.string().trim() + }) + } + }, + handler: async (req) => { + const pkiCollectionItem = await server.services.pkiCollection.removeItemFromPkiCollection({ + collectionId: req.params.collectionId, + itemId: req.params.itemId, + actor: req.permission.type, + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId + }); + + // TODO: audit logging + + // await server.services.auditLog.createAuditLog({ + // ...req.auditLogInfo, + // projectId: ca.projectId, + // event: { + // type: EventType.DELETE_CERT, + // metadata: { + // certId: deletedCert.id, + // cn: deletedCert.commonName, + // serialNumber: deletedCert.serialNumber + // } + // } + // }); + + return pkiCollectionItem; + } + }); }; diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 34a9eea31..85043ecee 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -2,9 +2,9 @@ import slugify from "@sindresorhus/slugify"; import { z } from "zod"; import { - AlertsSchema, CertificateAuthoritiesSchema, CertificatesSchema, + PkiAlertsSchema, PkiCollectionsSchema, ProjectKeysSchema } from "@app/db/schemas"; @@ -411,7 +411,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - alerts: z.array(AlertsSchema) + alerts: z.array(PkiAlertsSchema) }) } }, diff --git a/backend/src/services/alert/alert-dal.ts b/backend/src/services/alert/alert-dal.ts deleted file mode 100644 index 08e8710ef..000000000 --- a/backend/src/services/alert/alert-dal.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { TDbClient } from "@app/db"; -import { TableName } from "@app/db/schemas"; -import { ormify } from "@app/lib/knex"; - -export type TAlertDALFactory = ReturnType; - -export const alertDALFactory = (db: TDbClient) => { - const alertOrm = ormify(db, TableName.Alert); - return { - ...alertOrm - }; -}; diff --git a/backend/src/services/pki-alert/pki-alert-dal.ts b/backend/src/services/pki-alert/pki-alert-dal.ts new file mode 100644 index 000000000..7e12813e1 --- /dev/null +++ b/backend/src/services/pki-alert/pki-alert-dal.ts @@ -0,0 +1,12 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TPkiAlertDALFactory = ReturnType; + +export const pkiAlertDALFactory = (db: TDbClient) => { + const pkiAlertOrm = ormify(db, TableName.PkiAlert); + return { + ...pkiAlertOrm + }; +}; diff --git a/backend/src/services/alert/alert-service.ts b/backend/src/services/pki-alert/pki-alert-service.ts similarity index 84% rename from backend/src/services/alert/alert-service.ts rename to backend/src/services/pki-alert/pki-alert-service.ts index cd8501513..3692afe4d 100644 --- a/backend/src/services/alert/alert-service.ts +++ b/backend/src/services/pki-alert/pki-alert-service.ts @@ -5,18 +5,22 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services import { NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal"; -import { TAlertDALFactory } from "./alert-dal"; -import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./alert-types"; +import { TPkiAlertDALFactory } from "./pki-alert-dal"; +import { TCreateAlertDTO, TDeleteAlertDTO, TGetAlertByIdDTO, TUpdateAlertDTO } from "./pki-alert-types"; -type TAlertServiceFactoryDep = { - alertDAL: TAlertDALFactory; +type TPkiAlertServiceFactoryDep = { + pkiAlertDAL: TPkiAlertDALFactory; pkiCollectionDAL: TPkiCollectionDALFactory; permissionService: Pick; }; -export type TAlertServiceFactory = ReturnType; +export type TPkiAlertServiceFactory = ReturnType; -export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionService }: TAlertServiceFactoryDep) => { +export const pkiAlertServiceFactory = ({ + pkiAlertDAL, + pkiCollectionDAL, + permissionService +}: TPkiAlertServiceFactoryDep) => { const createPkiAlert = async ({ projectId, name, @@ -43,7 +47,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ if (pkiCollection.projectId !== projectId) throw new UnauthorizedError({ message: "PKI collection not found in project" }); - const alert = await alertDAL.create({ + const alert = await pkiAlertDAL.create({ projectId, pkiCollectionId, name, @@ -54,7 +58,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ }; const getPkiAlertById = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TGetAlertByIdDTO) => { - const alert = await alertDAL.findById(alertId); + const alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: "Alert not found" }); const { permission } = await permissionService.getProjectPermission( @@ -80,7 +84,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ actor, actorOrgId }: TUpdateAlertDTO) => { - let alert = await alertDAL.findById(alertId); + let alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: "Alert not found" }); const { permission } = await permissionService.getProjectPermission( @@ -100,7 +104,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ throw new UnauthorizedError({ message: "PKI collection not found in project" }); } - alert = await alertDAL.updateById(alertId, { + alert = await pkiAlertDAL.updateById(alertId, { name, alertBeforeDays, ...(pkiCollectionId && { pkiCollectionId }), @@ -111,7 +115,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ }; const deletePkiAlert = async ({ alertId, actorId, actorAuthMethod, actor, actorOrgId }: TDeleteAlertDTO) => { - let alert = await alertDAL.findById(alertId); + let alert = await pkiAlertDAL.findById(alertId); if (!alert) throw new NotFoundError({ message: "Alert not found" }); const { permission } = await permissionService.getProjectPermission( @@ -123,7 +127,7 @@ export const alertServiceFactory = ({ alertDAL, pkiCollectionDAL, permissionServ ); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts); - alert = await alertDAL.deleteById(alertId); + alert = await pkiAlertDAL.deleteById(alertId); return alert; }; diff --git a/backend/src/services/alert/alert-types.ts b/backend/src/services/pki-alert/pki-alert-types.ts similarity index 100% rename from backend/src/services/alert/alert-types.ts rename to backend/src/services/pki-alert/pki-alert-types.ts diff --git a/backend/src/services/pki-collection/pki-collection-fns.ts b/backend/src/services/pki-collection/pki-collection-fns.ts new file mode 100644 index 000000000..2b74bcf5b --- /dev/null +++ b/backend/src/services/pki-collection/pki-collection-fns.ts @@ -0,0 +1,30 @@ +import { TPkiCollectionItems } from "@app/db/schemas"; + +import { PkiItemType } from "./pki-collection-types"; + +/** + * Transforms a PKI Collection Item from the database to the expected API response format + */ +export const transformPkiCollectionItem = (pkiCollectionItem: TPkiCollectionItems) => { + let type: PkiItemType; + let itemId: string; + + if (pkiCollectionItem.caId) { + type = PkiItemType.CA; + itemId = pkiCollectionItem.caId; + } else if (pkiCollectionItem.certId) { + type = PkiItemType.CERTIFICATE; + itemId = pkiCollectionItem.certId; + } else { + throw new Error("Invalid PKI Collection Item: must have either caId or certId"); + } + + return { + id: pkiCollectionItem.id, + pkiCollectionId: pkiCollectionItem.pkiCollectionId, + type, + itemId, + createdAt: pkiCollectionItem.createdAt, + updatedAt: pkiCollectionItem.updatedAt + }; +}; diff --git a/backend/src/services/pki-collection/pki-collection-item-dal.ts b/backend/src/services/pki-collection/pki-collection-item-dal.ts new file mode 100644 index 000000000..57763aefb --- /dev/null +++ b/backend/src/services/pki-collection/pki-collection-item-dal.ts @@ -0,0 +1,33 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +export type TPkiCollectionItemDALFactory = ReturnType; + +export const pkiCollectionItemDALFactory = (db: TDbClient) => { + const pkiCollectionItemOrm = ormify(db, TableName.PkiCollectionItem); + + const countItemsInPkiCollection = async (collectionId: string) => { + try { + interface CountResult { + count: string; + } + + const query = db + .replicaNode()(TableName.PkiCollectionItem) + .where(`${TableName.PkiCollectionItem}.pkiCollectionId`, collectionId); + + const count = await query.count("*").first(); + + return parseInt((count as unknown as CountResult).count || "0", 10); + } catch (error) { + throw new DatabaseError({ error, name: "Count all project certificates" }); + } + }; + + return { + ...pkiCollectionItemOrm, + countItemsInPkiCollection + }; +}; diff --git a/backend/src/services/pki-collection/pki-collection-service.ts b/backend/src/services/pki-collection/pki-collection-service.ts index 35328b7e4..4ee0b4e44 100644 --- a/backend/src/services/pki-collection/pki-collection-service.ts +++ b/backend/src/services/pki-collection/pki-collection-service.ts @@ -1,19 +1,31 @@ import { ForbiddenError } from "@casl/ability"; +import { TPkiCollectionItems } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; -import { NotFoundError } from "@app/lib/errors"; +import { BadRequestError, NotFoundError } from "@app/lib/errors"; +import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; +import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TPkiCollectionDALFactory } from "./pki-collection-dal"; +import { transformPkiCollectionItem } from "./pki-collection-fns"; +import { TPkiCollectionItemDALFactory } from "./pki-collection-item-dal"; import { + PkiItemType, + TAddItemToPkiCollectionDTO, TCreatePkiCollectionDTO, TDeletePkiCollectionDTO, TGetPkiCollectionByIdDTO, + TGetPkiCollectionItems, + TRemoveItemFromPkiCollectionDTO, TUpdatePkiCollectionDTO } from "./pki-collection-types"; type TPkiCollectionServiceFactoryDep = { - pkiCollectionDAL: TPkiCollectionDALFactory; + pkiCollectionDAL: TPkiCollectionDALFactory; // TODO: Pick + pkiCollectionItemDAL: TPkiCollectionItemDALFactory; + certificateAuthorityDAL: TCertificateAuthorityDALFactory; + certificateDAL: TCertificateDALFactory; permissionService: Pick; }; @@ -21,6 +33,9 @@ export type TPkiCollectionServiceFactory = ReturnType { const createPkiCollection = async ({ @@ -127,12 +142,168 @@ export const pkiCollectionServiceFactory = ({ return pkiCollection; }; - // TODO: add/remove pki collection items + const getPkiCollectionItems = async ({ + collectionId, + offset = 0, + limit = 25, + actorId, + actorAuthMethod, + actor, + actorOrgId + }: TGetPkiCollectionItems) => { + const pkiCollection = await pkiCollectionDAL.findById(collectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + pkiCollection.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections); + + const pkiCollectionItems = await pkiCollectionItemDAL.find( + { pkiCollectionId: collectionId }, + { offset, limit, sort: [["createdAt", "desc"]] } + ); + + const count = await pkiCollectionItemDAL.countItemsInPkiCollection(collectionId); + + return { + pkiCollectionItems: pkiCollectionItems.map(transformPkiCollectionItem), + totalCount: count + }; + }; + + const addItemToPkiCollection = async ({ + collectionId, + actorId, + actorAuthMethod, + actor, + actorOrgId, + type, + itemId + }: TAddItemToPkiCollectionDTO) => { + const pkiCollection = await pkiCollectionDAL.findById(collectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + pkiCollection.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + ProjectPermissionSub.PkiCollections + ); + + let pkiCollectionItem: TPkiCollectionItems; + switch (type) { + case PkiItemType.CA: { + // validate that CA has not already been added to PKI collection + const isCaAdded = await pkiCollectionItemDAL.findOne({ + pkiCollectionId: collectionId, + caId: itemId + }); + + if (isCaAdded) throw new BadRequestError({ message: "CA is already part of the PKI collection" }); + + // validate that there exists a CA in same project as PKI collection + const ca = await certificateAuthorityDAL.findOne({ + id: itemId, + projectId: pkiCollection.projectId + }); + + if (!ca) throw new NotFoundError({ message: "CA not found" }); + + pkiCollectionItem = await pkiCollectionItemDAL.create({ + pkiCollectionId: collectionId, + caId: itemId + }); + break; + } + case PkiItemType.CERTIFICATE: { + // validate that certificate has not already been added to PKI collection + const isCertAdded = await pkiCollectionItemDAL.findOne({ + pkiCollectionId: collectionId, + certId: itemId + }); + if (isCertAdded) throw new BadRequestError({ message: "Certificate already part of the PKI collection" }); + + // validate that there exists a certificate in same project as PKI collection + const cas = await certificateAuthorityDAL.find({ projectId: pkiCollection.projectId }); + + // TODO: consider making this more efficient + const [certificate] = await certificateDAL.find({ + $in: { + caId: cas.map((ca) => ca.id) + }, + id: itemId + }); + if (!certificate) throw new NotFoundError({ message: "Certificate not found" }); + + pkiCollectionItem = await pkiCollectionItemDAL.create({ + pkiCollectionId: collectionId, + certId: itemId + }); + break; + } + default: { + throw new BadRequestError({ message: "Invalid PKI item type" }); + } + } + + return transformPkiCollectionItem(pkiCollectionItem); + }; + + const removeItemFromPkiCollection = async ({ + collectionId, + actorId, + actorAuthMethod, + actor, + actorOrgId, + itemId + }: TRemoveItemFromPkiCollectionDTO) => { + const pkiCollection = await pkiCollectionDAL.findById(collectionId); + if (!pkiCollection) throw new NotFoundError({ message: "PKI collection not found" }); + + let pkiCollectionItem = await pkiCollectionItemDAL.findOne({ + pkiCollectionId: collectionId, + id: itemId + }); + + if (!pkiCollectionItem) throw new NotFoundError({ message: "PKI collection item not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + pkiCollection.projectId, + actorAuthMethod, + actorOrgId + ); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + ProjectPermissionSub.PkiCollections + ); + + pkiCollectionItem = await pkiCollectionItemDAL.deleteById(itemId); + + return transformPkiCollectionItem(pkiCollectionItem); + }; return { createPkiCollection, getPkiCollectionById, updatePkiCollection, - deletePkiCollection + deletePkiCollection, + getPkiCollectionItems, + addItemToPkiCollection, + removeItemFromPkiCollection }; }; diff --git a/backend/src/services/pki-collection/pki-collection-types.ts b/backend/src/services/pki-collection/pki-collection-types.ts index 9281a1721..ec7ce3cfa 100644 --- a/backend/src/services/pki-collection/pki-collection-types.ts +++ b/backend/src/services/pki-collection/pki-collection-types.ts @@ -16,3 +16,25 @@ export type TUpdatePkiCollectionDTO = { export type TDeletePkiCollectionDTO = { collectionId: string; } & Omit; + +export enum PkiItemType { + CERTIFICATE = "certificate", + CA = "ca" +} + +export type TGetPkiCollectionItems = { + collectionId: string; + offset: number; + limit: number; +} & Omit; + +export type TAddItemToPkiCollectionDTO = { + collectionId: string; + type: PkiItemType; + itemId: string; +} & Omit; + +export type TRemoveItemFromPkiCollectionDTO = { + collectionId: string; + itemId: string; +} & Omit; diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 699b22a97..9416a66a9 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -13,7 +13,6 @@ import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/ import { alphaNumericNanoId } from "@app/lib/nanoid"; import { TProjectPermission } from "@app/lib/types"; -import { TAlertDALFactory } from "../alert/alert-dal"; import { ActorType } from "../auth/auth-type"; import { TCertificateDALFactory } from "../certificate/certificate-dal"; import { TCertificateAuthorityDALFactory } from "../certificate-authority/certificate-authority-dal"; @@ -23,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id import { TKmsServiceFactory } from "../kms/kms-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; +import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -73,7 +73,7 @@ type TProjectServiceFactoryDep = { projectUserMembershipRoleDAL: Pick; certificateAuthorityDAL: Pick; certificateDAL: Pick; - alertDAL: Pick; + pkiAlertDAL: Pick; pkiCollectionDAL: Pick; permissionService: TPermissionServiceFactory; orgService: Pick; @@ -113,7 +113,7 @@ export const projectServiceFactory = ({ certificateAuthorityDAL, certificateDAL, pkiCollectionDAL, - alertDAL, + pkiAlertDAL, keyStore, kmsService, projectBotDAL @@ -684,7 +684,7 @@ export const projectServiceFactory = ({ }; /** - * Return list of alerts configured for project + * Return list of (PKI) alerts configured for project */ const listProjectAlerts = async ({ projectId, @@ -703,7 +703,7 @@ export const projectServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts); - const alerts = await alertDAL.find({ projectId }); + const alerts = await pkiAlertDAL.find({ projectId }); return { alerts diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 04edd80e4..c307688d6 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -43,7 +43,6 @@ export type ProjectPermissionSet = ProjectPermissionActions, ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) ] - | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] @@ -60,6 +59,8 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] + | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] diff --git a/frontend/src/hooks/api/pkiAlerts/index.tsx b/frontend/src/hooks/api/pkiAlerts/index.tsx index 8bb912094..f88f71679 100644 --- a/frontend/src/hooks/api/pkiAlerts/index.tsx +++ b/frontend/src/hooks/api/pkiAlerts/index.tsx @@ -1,2 +1,2 @@ -export { useCreateAlert, useDeleteAlert,useUpdateAlert } from "./mutations"; -export { useGetAlertById } from "./queries"; +export { useCreatePkiAlert, useDeletePkiAlert, useUpdatePkiAlert } from "./mutations"; +export { useGetPkiAlertById } from "./queries"; diff --git a/frontend/src/hooks/api/pkiAlerts/mutations.tsx b/frontend/src/hooks/api/pkiAlerts/mutations.tsx index 6e08fe14b..b8406e95d 100644 --- a/frontend/src/hooks/api/pkiAlerts/mutations.tsx +++ b/frontend/src/hooks/api/pkiAlerts/mutations.tsx @@ -6,7 +6,7 @@ import { workspaceKeys } from "../workspace/queries"; import { pkiAlertKeys } from "./queries"; import { TCreatePkiAlertDTO, TDeletePkiAlertDTO, TPkiAlert, TUpdatePkiAlertDTO } from "./types"; -export const useCreateAlert = () => { +export const useCreatePkiAlert = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async (body) => { @@ -19,7 +19,7 @@ export const useCreateAlert = () => { }); }; -export const useUpdateAlert = () => { +export const useUpdatePkiAlert = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ alertId, ...body }) => { @@ -36,7 +36,7 @@ export const useUpdateAlert = () => { }); }; -export const useDeleteAlert = () => { +export const useDeletePkiAlert = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ alertId }) => { diff --git a/frontend/src/hooks/api/pkiAlerts/queries.tsx b/frontend/src/hooks/api/pkiAlerts/queries.tsx index cd891f9a1..db324e96d 100644 --- a/frontend/src/hooks/api/pkiAlerts/queries.tsx +++ b/frontend/src/hooks/api/pkiAlerts/queries.tsx @@ -8,7 +8,7 @@ export const pkiAlertKeys = { getPkiAlertById: (alertId: string) => [{ alertId }, "alert"] }; -export const useGetAlertById = (alertId: string) => { +export const useGetPkiAlertById = (alertId: string) => { return useQuery({ queryKey: pkiAlertKeys.getPkiAlertById(alertId), queryFn: async () => { diff --git a/frontend/src/hooks/api/pkiCollections/constants.tsx b/frontend/src/hooks/api/pkiCollections/constants.tsx new file mode 100644 index 000000000..c103dc386 --- /dev/null +++ b/frontend/src/hooks/api/pkiCollections/constants.tsx @@ -0,0 +1,9 @@ +export enum PkiItemType { + CERTIFICATE = "certificate", + CA = "ca" +} + +export const pkiItemTypeToNameMap: { [K in PkiItemType]: string } = { + [PkiItemType.CA]: "CA", + [PkiItemType.CERTIFICATE]: "Certificate" +}; diff --git a/frontend/src/hooks/api/pkiCollections/index.tsx b/frontend/src/hooks/api/pkiCollections/index.tsx index 61f332507..cf638d8fa 100644 --- a/frontend/src/hooks/api/pkiCollections/index.tsx +++ b/frontend/src/hooks/api/pkiCollections/index.tsx @@ -1,5 +1,7 @@ export { + useAddItemToPkiCollection, useCreatePkiCollection, useDeletePkiCollection, + useRemoveItemFromPkiCollection, useUpdatePkiCollection} from "./mutations"; -export { useGetPkiCollectionById } from "./queries"; +export { useGetPkiCollectionById, useListPkiCollectionItems } from "./queries"; diff --git a/frontend/src/hooks/api/pkiCollections/mutations.tsx b/frontend/src/hooks/api/pkiCollections/mutations.tsx index 23de0fca4..df66dd8e2 100644 --- a/frontend/src/hooks/api/pkiCollections/mutations.tsx +++ b/frontend/src/hooks/api/pkiCollections/mutations.tsx @@ -3,13 +3,14 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace/queries"; -// import { alertKeys } from "./queries"; -// import { TAlert, TCreateAlertDTO, TDeleteAlertDTO, TUpdateAlertDTO } from "./types"; import { pkiCollectionKeys } from "./queries"; import { + TAddItemToPkiCollectionDTO, TCreatePkiCollectionDTO, TDeletePkiCollectionDTO, TPkiCollection, + TPkiCollectionItem, + TRemoveItemFromPkiCollectionDTO, TUpdatePkiCollectionTO} from "./types"; export const useCreatePkiCollection = () => { @@ -61,4 +62,36 @@ export const useDeletePkiCollection = () => { }); }; -// TODO: add PKI Collection Item +export const useAddItemToPkiCollection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ collectionId, type, itemId }) => { + const { data: pkiCollectionItem } = await apiRequest.post( + `/api/v1/pki/collections/${collectionId}/items`, + { + type, + itemId + } + ); + return pkiCollectionItem; + }, + onSuccess: (_, { collectionId }) => { + queryClient.invalidateQueries(pkiCollectionKeys.getPkiCollectionItems(collectionId)); + } + }); +}; + +export const useRemoveItemFromPkiCollection = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ collectionId, itemId }) => { + const { data: pkiCollectionItem } = await apiRequest.delete( + `/api/v1/pki/collections/${collectionId}/items/${itemId}` + ); + return pkiCollectionItem; + }, + onSuccess: (_, { collectionId }) => { + queryClient.invalidateQueries(pkiCollectionKeys.getPkiCollectionItems(collectionId)); + } + }); +}; diff --git a/frontend/src/hooks/api/pkiCollections/queries.tsx b/frontend/src/hooks/api/pkiCollections/queries.tsx index 004458d23..c5013fcf3 100644 --- a/frontend/src/hooks/api/pkiCollections/queries.tsx +++ b/frontend/src/hooks/api/pkiCollections/queries.tsx @@ -2,10 +2,26 @@ import { useQuery } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TPkiCollection } from "./types"; +import { TPkiCollection, TPkiCollectionItem } from "./types"; export const pkiCollectionKeys = { - getPkiCollectionById: (collectionId: string) => [{ collectionId }, "pki-collection"] + getPkiCollectionById: (collectionId: string) => [{ collectionId }, "pki-collection"] as const, + getPkiCollectionItems: (collectionId: string) => + [{ collectionId }, "pki-collection-items"] as const, + specificPkiCollectionItems: ({ + collectionId, + offset, + limit + }: { + collectionId: string; + offset: number; + limit: number; + }) => + [ + ...pkiCollectionKeys.getPkiCollectionItems(collectionId), + { offset, limit }, + "pki-collection-items-2" + ] as const }; export const useGetPkiCollectionById = (collectionId: string) => { @@ -20,3 +36,39 @@ export const useGetPkiCollectionById = (collectionId: string) => { enabled: Boolean(collectionId) }); }; + +export const useListPkiCollectionItems = ({ + collectionId, + offset, + limit +}: { + collectionId: string; + offset: number; + limit: number; +}) => { + return useQuery({ + queryKey: pkiCollectionKeys.specificPkiCollectionItems({ + collectionId, + offset, + limit + }), + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit) + }); + + const { + data: { collectionItems, totalCount } + } = await apiRequest.get<{ + collectionItems: TPkiCollectionItem[]; + totalCount: number; + }>(`/api/v1/pki/collections/${collectionId}/items`, { + params + }); + + return { collectionItems, totalCount }; + }, + enabled: Boolean(collectionId) + }); +}; diff --git a/frontend/src/hooks/api/pkiCollections/types.ts b/frontend/src/hooks/api/pkiCollections/types.ts index e372d80da..86c2471e9 100644 --- a/frontend/src/hooks/api/pkiCollections/types.ts +++ b/frontend/src/hooks/api/pkiCollections/types.ts @@ -21,3 +21,23 @@ export type TDeletePkiCollectionDTO = { collectionId: string; projectId: string; }; + +export type TPkiCollectionItem = { + id: string; + collectionId: string; + type: string; + itemId: string; + createdAt: string; + updatedAt: string; +}; + +export type TAddItemToPkiCollectionDTO = { + collectionId: string; + type: string; + itemId: string; +}; + +export type TRemoveItemFromPkiCollectionDTO = { + collectionId: string; + itemId: string; +}; diff --git a/frontend/src/hooks/api/workspace/index.tsx b/frontend/src/hooks/api/workspace/index.tsx index 81fb0f1be..3da83e1b8 100644 --- a/frontend/src/hooks/api/workspace/index.tsx +++ b/frontend/src/hooks/api/workspace/index.tsx @@ -23,10 +23,10 @@ export { useGetWorkspaceIntegrations, useGetWorkspaceSecrets, useGetWorkspaceUsers, - useListWorkspaceAlerts, useListWorkspaceCas, useListWorkspaceCertificates, useListWorkspaceGroups, + useListWorkspacePkiAlerts, useListWorkspacePkiCollections, useNameWorkspaceSecrets, useRenameWorkspace, diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index 51722111c..d971f06e2 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -65,7 +65,8 @@ export const workspaceKeys = { offset: number; limit: number; }) => [...workspaceKeys.forWorkspaceCertificates(slug), { offset, limit }] as const, - getWorkspacePkiAlerts: (workspaceId: string) => [{ workspaceId }, "workspace-alerts"] as const, + getWorkspacePkiAlerts: (workspaceId: string) => + [{ workspaceId }, "workspace-pki-alerts"] as const, getWorkspacePkiCollections: (workspaceId: string) => [{ workspaceId }, "workspace-pki-collections"] as const }; @@ -607,7 +608,7 @@ export const useListWorkspaceCertificates = ({ }); }; -export const useListWorkspaceAlerts = ({ workspaceId }: { workspaceId: string }) => { +export const useListWorkspacePkiAlerts = ({ workspaceId }: { workspaceId: string }) => { return useQuery({ queryKey: workspaceKeys.getWorkspacePkiAlerts(workspaceId), queryFn: async () => { diff --git a/frontend/src/pages/project/[id]/pki-collections/[collectionId]/index.tsx b/frontend/src/pages/project/[id]/pki-collections/[collectionId]/index.tsx new file mode 100644 index 000000000..10533fd31 --- /dev/null +++ b/frontend/src/pages/project/[id]/pki-collections/[collectionId]/index.tsx @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; + +import { PkiCollectionPage } from "@app/views/Project/PkiCollectionPage"; + +export default function PkiCollection() { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "PKI Collection" })} + + + + + ); +} + +PkiCollection.requireAuth = true; diff --git a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx index 5fd5ee8d9..82fa42871 100644 --- a/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx +++ b/frontend/src/views/Project/CertificatesPage/CertificatesPage.tsx @@ -2,7 +2,7 @@ import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { withProjectPermission } from "@app/hoc"; -import { AlertsTab, CaTab, CertificatesTab } from "./components"; +import { CaTab, CertificatesTab,PkiAlertsTab } from "./components"; enum TabSections { Ca = "certificate-authorities", @@ -29,7 +29,7 @@ export const CertificatesPage = withProjectPermission( - + diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/index.tsx b/frontend/src/views/Project/CertificatesPage/components/AlertsTab/index.tsx deleted file mode 100644 index 6bfffb6ee..000000000 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AlertsTab } from "./AlertsTab"; diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx similarity index 71% rename from frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx rename to frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx index 8d1b12d52..974c2aa1b 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/AlertsTab.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/PkiAlertsTab.tsx @@ -1,8 +1,8 @@ import { motion } from "framer-motion"; -import { AlertsSection,PkiCollectionSection } from "./components"; +import { PkiAlertsSection, PkiCollectionSection } from "./components"; -export const AlertsTab = () => { +export const PkiAlertsTab = () => { return ( { exit={{ opacity: 0, translateX: 30 }} > - + ); }; diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx similarity index 88% rename from frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx rename to frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx index 6bcc56875..62ae89476 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertModal.tsx @@ -12,13 +12,15 @@ import { ModalContent, Select, SelectItem, - TextArea} from "@app/components/v2"; + TextArea +} from "@app/components/v2"; import { useWorkspace } from "@app/context"; import { - useCreateAlert, - useGetAlertById, + useCreatePkiAlert, + useGetPkiAlertById, useListWorkspacePkiCollections, - useUpdateAlert} from "@app/hooks/api"; + useUpdatePkiAlert +} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; enum TimeUnit { @@ -54,24 +56,24 @@ const convertToDays = (unit: TimeUnit, value: number) => { export type FormData = z.infer; type Props = { - popUp: UsePopUpState<["alert"]>; - handlePopUpToggle: (popUpName: keyof UsePopUpState<["alert"]>, state?: boolean) => void; + popUp: UsePopUpState<["pkiAlert"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["pkiAlert"]>, state?: boolean) => void; }; -export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { +export const PkiAlertModal = ({ popUp, handlePopUpToggle }: Props) => { const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { data: alert } = useGetAlertById( - (popUp?.alert?.data as { alertId: string })?.alertId || "" + const { data: alert } = useGetPkiAlertById( + (popUp?.pkiAlert?.data as { alertId: string })?.alertId || "" ); const { data: pkiCollections } = useListWorkspacePkiCollections({ workspaceId: projectId }); - const { mutateAsync: createAlert } = useCreateAlert(); - const { mutateAsync: updateAlert } = useUpdateAlert(); + const { mutateAsync: createPkiAlert } = useCreatePkiAlert(); + const { mutateAsync: updatePkiAlert } = useUpdatePkiAlert(); const { control, @@ -95,11 +97,15 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { emails: alert.recipientEmails }); } else { + // TODO: add default collection? reset({ - name: "" + name: "", + ...(pkiCollections?.collections?.[0] && { + pkiCollectionId: pkiCollections.collections[0].id + }) }); } - }, [alert]); + }, [alert, pkiCollections]); const onFormSubmit = async ({ name, @@ -120,7 +126,7 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { if (alert) { // update - await updateAlert({ + await updatePkiAlert({ alertId: alert.id, pkiCollectionId, name, @@ -130,7 +136,7 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { }); } else { // create - await createAlert({ + await createPkiAlert({ name, projectId, pkiCollectionId, @@ -139,7 +145,7 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { }); } - handlePopUpToggle("alert", false); + handlePopUpToggle("pkiAlert", false); reset(); @@ -158,9 +164,9 @@ export const AlertModal = ({ popUp, handlePopUpToggle }: Props) => { return ( { - handlePopUpToggle("alert", isOpen); + handlePopUpToggle("pkiAlert", isOpen); reset(); }} > diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertRow.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertRow.tsx similarity index 89% rename from frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertRow.tsx rename to frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertRow.tsx index b3f9d322e..2eef7e3dc 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertRow.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertRow.tsx @@ -18,13 +18,16 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; type Props = { alert: TPkiAlert; - handlePopUpOpen: (popUpName: keyof UsePopUpState<["alert", "deleteAlert"]>, data?: {}) => void; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["pkiAlert", "deletePkiAlert"]>, + data?: {} + ) => void; }; -export const AlertRow = ({ alert, handlePopUpOpen }: Props) => { +export const PkiAlertRow = ({ alert, handlePopUpOpen }: Props) => { const { data: pkiCollection } = useGetPkiCollectionById(alert.pkiCollectionId || ""); return ( - + {alert.name} {alert.alertBeforeDays} {pkiCollection ? pkiCollection.name : "-"} @@ -47,7 +50,7 @@ export const AlertRow = ({ alert, handlePopUpOpen }: Props) => { )} onClick={(e) => { e.stopPropagation(); - handlePopUpOpen("alert", { + handlePopUpOpen("pkiAlert", { alertId: alert.id }); }} @@ -70,7 +73,7 @@ export const AlertRow = ({ alert, handlePopUpOpen }: Props) => { )} onClick={(e) => { e.stopPropagation(); - handlePopUpOpen("deleteAlert", { + handlePopUpOpen("deletePkiAlert", { alertId: alert.id, name: alert.name }); diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertsSection.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsSection.tsx similarity index 69% rename from frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertsSection.tsx rename to frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsSection.tsx index 5b4de2336..af27c4b7a 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertsSection.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsSection.tsx @@ -5,37 +5,37 @@ import { createNotification } from "@app/components/notifications"; import { ProjectPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { useDeleteAlert } from "@app/hooks/api"; +import { useDeletePkiAlert } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; -import { AlertModal } from "./AlertModal"; -import { AlertsTable } from "./AlertsTable"; +import { PkiAlertModal } from "./PkiAlertModal"; +import { PkiAlertsTable } from "./PkiAlertsTable"; -export const AlertsSection = () => { +export const PkiAlertsSection = () => { const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { mutateAsync: deleteAlert } = useDeleteAlert(); + const { mutateAsync: deletePkiAlert } = useDeletePkiAlert(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "alert", - "deleteAlert" + "pkiAlert", + "deletePkiAlert" ] as const); const onRemoveAlertSubmit = async (alertId: string) => { try { if (!projectId) return; - await deleteAlert({ + await deletePkiAlert({ alertId, projectId }); - await createNotification({ + createNotification({ text: "Successfully deleted alert", type: "success" }); - handlePopUpClose("deleteAlert"); + handlePopUpClose("deletePkiAlert"); } catch (err) { console.error(err); createNotification({ @@ -58,7 +58,7 @@ export const AlertsSection = () => { colorSchema="primary" type="submit" leftIcon={} - onClick={() => handlePopUpOpen("alert")} + onClick={() => handlePopUpOpen("pkiAlert")} isDisabled={!isAllowed} > Create @@ -66,17 +66,17 @@ export const AlertsSection = () => { )} - - + + handlePopUpToggle("deleteAlert", isOpen)} + onChange={(isOpen) => handlePopUpToggle("deletePkiAlert", isOpen)} deleteKey="confirm" onDeleteApproved={() => - onRemoveAlertSubmit((popUp?.deleteAlert?.data as { alertId: string })?.alertId) + onRemoveAlertSubmit((popUp?.deletePkiAlert?.data as { alertId: string })?.alertId) } /> diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertsTable.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsTable.tsx similarity index 67% rename from frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertsTable.tsx rename to frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsTable.tsx index dcf897da0..f31b246f1 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/AlertsTable.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiAlertsTable.tsx @@ -11,20 +11,23 @@ import { Tr } from "@app/components/v2"; import { useWorkspace } from "@app/context"; -import { useListWorkspaceAlerts } from "@app/hooks/api"; +import { useListWorkspacePkiAlerts } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; -import { AlertRow } from "./AlertRow"; +import { PkiAlertRow } from "./PkiAlertRow"; type Props = { - handlePopUpOpen: (popUpName: keyof UsePopUpState<["alert", "deleteAlert"]>, data?: {}) => void; + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["pkiAlert", "deletePkiAlert"]>, + data?: {} + ) => void; }; -export const AlertsTable = ({ handlePopUpOpen }: Props) => { +export const PkiAlertsTable = ({ handlePopUpOpen }: Props) => { const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; - const { data, isLoading } = useListWorkspaceAlerts({ + const { data, isLoading } = useListWorkspacePkiAlerts({ workspaceId: projectId }); @@ -35,16 +38,16 @@ export const AlertsTable = ({ handlePopUpOpen }: Props) => { Alert Name Alert Before Days - Bound PKI Collection + Certificate Collection - {isLoading && } + {isLoading && } {!isLoading && data?.alerts.map((alert) => { return ( - { {!isLoading && !data?.alerts?.length && ( - + )} ); diff --git a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/PkiCollectionModal.tsx b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal.tsx similarity index 92% rename from frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/PkiCollectionModal.tsx rename to frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal.tsx index b7cdc424b..89fd51c92 100644 --- a/frontend/src/views/Project/CertificatesPage/components/AlertsTab/components/PkiCollectionModal.tsx +++ b/frontend/src/views/Project/CertificatesPage/components/PkiAlertsTab/components/PkiCollectionModal.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { Controller, useForm } from "react-hook-form"; +import { useRouter } from "next/router"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -25,6 +26,7 @@ type Props = { }; export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { + const router = useRouter(); const { currentWorkspace } = useWorkspace(); const projectId = currentWorkspace?.id || ""; @@ -69,10 +71,12 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { }); } else { // create - await createPkiCollection({ + const { id: createdId } = await createPkiCollection({ name, projectId }); + + router.push(`/project/${projectId}/pki-collections/${createdId}`); } handlePopUpToggle("pkiCollection", false); @@ -100,7 +104,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => { reset(); }} > - +
{ isLoading={isSubmitting} isDisabled={isSubmitting} > - Create + {pkiCollection ? "Update" : "Create"} +
+

{data.name}

+ + +
+ + + +
+
+ + + {(isAllowed) => ( + { + handlePopUpOpen("pkiCollection", { + collectionId + }); + }} + disabled={!isAllowed} + > + Edit PKI Collection + + )} + + + {(isAllowed) => ( + + handlePopUpOpen("deletePkiCollection", { + collectionId: data.id, + name: data.name + }) + } + disabled={!isAllowed} + > + Delete PKI Collection + + )} + + +
+
+
+
+ +
+ +
+ + )} + + handlePopUpToggle("deletePkiCollection", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onDeletePkiCollectionSubmit( + (popUp.deletePkiCollection.data as { collectionId: string })?.collectionId + ) + } + /> + + ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.PkiCollections } +); diff --git a/frontend/src/views/Project/PkiCollectionPage/components/AddPkiCollectionItemModal.tsx b/frontend/src/views/Project/PkiCollectionPage/components/AddPkiCollectionItemModal.tsx new file mode 100644 index 000000000..471bb6a7a --- /dev/null +++ b/frontend/src/views/Project/PkiCollectionPage/components/AddPkiCollectionItemModal.tsx @@ -0,0 +1,199 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { + CaStatus, + useAddItemToPkiCollection, + useListWorkspaceCas, + useListWorkspaceCertificates} from "@app/hooks/api"; +import { PkiItemType, pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const schema = z + .object({ + type: z.nativeEnum(PkiItemType), + itemId: z.string() + }) + .required(); + +type FormData = z.infer; + +type Props = { + collectionId: string; + popUp: UsePopUpState<["addPkiCollectionItem"]>; + handlePopUpToggle: ( + popUpName: keyof UsePopUpState<["addPkiCollectionItem"]>, + state?: boolean + ) => void; +}; + +// note: this component should be optimized so it is easier +// to find certificates and CAs +export const AddPkiCollectionItemModal = ({ collectionId, popUp, handlePopUpToggle }: Props) => { + const { currentWorkspace } = useWorkspace(); + + const { data: cas } = useListWorkspaceCas({ + projectSlug: currentWorkspace?.slug || "", + status: CaStatus.ACTIVE + }); + + const { data } = useListWorkspaceCertificates({ + projectSlug: currentWorkspace?.slug || "", + offset: 0, + limit: 25 + }); + + const { mutateAsync: addItemToPkiCollection } = useAddItemToPkiCollection(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting }, + watch + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + type: PkiItemType.CA + } + }); + + const itemType = watch("type"); + + const onFormSubmit = async ({ type, itemId }: FormData) => { + try { + const item = await addItemToPkiCollection({ + collectionId, + type, + itemId + }); + + createNotification({ + text: `Successfully added ${ + pkiItemTypeToNameMap[item.type as PkiItemType] + } to PKI collection`, + type: "success" + }); + + reset(); + handlePopUpToggle("addPkiCollectionItem", false); + } catch (err) { + console.error(err); + } + }; + + return ( + { + handlePopUpToggle("addPkiCollectionItem", isOpen); + reset(); + }} + > + + + ( + + + + )} + /> + {itemType === PkiItemType.CA && ( + ( + + + + )} + /> + )} + {itemType === PkiItemType.CERTIFICATE && ( + ( + + + + )} + /> + )} +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionDetailsSection.tsx b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionDetailsSection.tsx new file mode 100644 index 000000000..bceda8511 --- /dev/null +++ b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionDetailsSection.tsx @@ -0,0 +1,83 @@ +import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { IconButton, Tooltip } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { useTimedReset } from "@app/hooks"; +import { useGetPkiCollectionById } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + collectionId: string; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["pkiCollection"]>, data?: {}) => void; +}; + +export const PkiCollectionDetailsSection = ({ collectionId, handlePopUpOpen }: Props) => { + const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset({ + initialState: "Copy ID to clipboard" + }); + + const { data: pkiCollection } = useGetPkiCollectionById(collectionId); + + return pkiCollection ? ( +
+
+

Collection Details

+ + {(isAllowed) => { + return ( + + + handlePopUpOpen("pkiCollection", { + collectionId + }) + } + > + + + + ); + }} + +
+
+
+

PKI Collection ID

+
+

{pkiCollection.id}

+
+ + { + navigator.clipboard.writeText(pkiCollection.id); + setCopyTextId("Copied"); + }} + > + + + +
+
+
+
+

Name

+

{pkiCollection.name}

+
+
+
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsSection.tsx b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsSection.tsx new file mode 100644 index 000000000..0236283d2 --- /dev/null +++ b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsSection.tsx @@ -0,0 +1,83 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { DeleteActionModal, IconButton } from "@app/components/v2"; +import { useGetPkiCollectionById, useRemoveItemFromPkiCollection } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { AddPkiCollectionItemModal } from "./AddPkiCollectionItemModal"; +import { PkiCollectionItemsTable } from "./PkiCollectionItemsTable"; + +type Props = { + collectionId: string; +}; + +export const PkiCollectionItemsSection = ({ collectionId }: Props) => { + const { data: pkiCollection } = useGetPkiCollectionById(collectionId); + const { mutateAsync: removeItemFromPkiCollection } = useRemoveItemFromPkiCollection(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deletePkiCollectionItem", + "addPkiCollectionItem" + ] as const); + + const onRemovePkiCollectionItemSubmit = async (itemId: string) => { + try { + await removeItemFromPkiCollection({ + itemId, + collectionId + }); + + createNotification({ + text: "Successfully removed item from PKI collection", + type: "success" + }); + + handlePopUpClose("deletePkiCollectionItem"); + } catch (err) { + console.error(err); + } + }; + + return pkiCollection ? ( +
+
+

Items

+ { + handlePopUpOpen("addPkiCollectionItem"); + }} + > + + +
+
+ +
+ + handlePopUpToggle("deletePkiCollectionItem", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => { + const popupData = popUp?.deletePkiCollectionItem?.data as { + itemId: string; + }; + + return onRemovePkiCollectionItemSubmit(popupData.itemId); + }} + /> +
+ ) : ( +
+ ); +}; diff --git a/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx new file mode 100644 index 000000000..ff45ce3f9 --- /dev/null +++ b/frontend/src/views/Project/PkiCollectionPage/components/PkiCollectionItemsTable.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; +import { faBoxesStacked, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + EmptyState, + IconButton, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { useListPkiCollectionItems } from "@app/hooks/api"; +import { PkiItemType,pkiItemTypeToNameMap } from "@app/hooks/api/pkiCollections/constants"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + collectionId: string; + handlePopUpOpen: (popUpName: keyof UsePopUpState<["deletePkiCollectionItem"]>, data?: {}) => void; +}; + +const PER_PAGE_INIT = 25; + +export const PkiCollectionItemsTable = ({ collectionId, handlePopUpOpen }: Props) => { + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(PER_PAGE_INIT); + + const { data, isLoading } = useListPkiCollectionItems({ + collectionId, + offset: (page - 1) * perPage, + limit: perPage + }); + + return ( +
+ + + + + + + + + + {isLoading && } + {!isLoading && + data?.collectionItems.map((collectionItem) => { + return ( + + + + + + ); + })} + +
ResourceID +
{pkiItemTypeToNameMap[collectionItem.type as PkiItemType]}{collectionItem.itemId} +
+ + {(isAllowed) => ( + + { + e.stopPropagation(); + handlePopUpOpen("deletePkiCollectionItem", { + collectionId, + itemId: collectionItem.id + }); + }} + > + + + + )} + +
+
+ {!isLoading && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isLoading && !data?.collectionItems?.length && ( + + )} +
+
+ ); +}; diff --git a/frontend/src/views/Project/PkiCollectionPage/components/index.tsx b/frontend/src/views/Project/PkiCollectionPage/components/index.tsx new file mode 100644 index 000000000..97ec4c0e0 --- /dev/null +++ b/frontend/src/views/Project/PkiCollectionPage/components/index.tsx @@ -0,0 +1,2 @@ +export { PkiCollectionDetailsSection } from "./PkiCollectionDetailsSection"; +export { PkiCollectionItemsSection } from "./PkiCollectionItemsSection"; diff --git a/frontend/src/views/Project/PkiCollectionPage/index.tsx b/frontend/src/views/Project/PkiCollectionPage/index.tsx new file mode 100644 index 000000000..acc175c14 --- /dev/null +++ b/frontend/src/views/Project/PkiCollectionPage/index.tsx @@ -0,0 +1 @@ +export { PkiCollectionPage } from "./PkiCollectionPage";