From c519cee5d1d84468acb9559a0615e8ec2e587b98 Mon Sep 17 00:00:00 2001 From: x032205 Date: Thu, 15 May 2025 16:32:57 -0400 Subject: [PATCH] frontend --- .../20250515164622_select-org-products.ts | 21 +++ backend/src/db/schemas/organizations.ts | 6 +- backend/src/server/routes/v1/index.ts | 4 +- .../server/routes/v1/organization-router.ts | 9 +- backend/src/server/routes/v3/index.ts | 2 +- backend/src/services/org/org-schema.ts | 6 +- backend/src/services/org/org-service.ts | 12 +- backend/src/services/org/org-types.ts | 4 + .../src/hooks/api/organization/queries.tsx | 12 +- frontend/src/hooks/api/organization/types.ts | 8 ++ .../MinimizedOrgSidebar.tsx | 120 ++++++++++-------- .../OrgGeneralTab/OrgGeneralTab.tsx | 14 +- .../OrgProductSelectSection.tsx | 84 ++++++++++++ .../OrgProductSelectSection/index.tsx | 1 + 14 files changed, 232 insertions(+), 71 deletions(-) create mode 100644 backend/src/db/migrations/20250515164622_select-org-products.ts create mode 100644 frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx create mode 100644 frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/index.tsx diff --git a/backend/src/db/migrations/20250515164622_select-org-products.ts b/backend/src/db/migrations/20250515164622_select-org-products.ts new file mode 100644 index 000000000..f143d7c0e --- /dev/null +++ b/backend/src/db/migrations/20250515164622_select-org-products.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.boolean("secretsProductEnabled").defaultTo(true); + t.boolean("pkiProductEnabled").defaultTo(true); + t.boolean("kmsProductEnabled").defaultTo(true); + t.boolean("sshProductEnabled").defaultTo(true); + }); +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(TableName.Organization, (t) => { + t.dropColumn("secretsProductEnabled"); + t.dropColumn("pkiProductEnabled"); + t.dropColumn("kmsProductEnabled"); + t.dropColumn("sshProductEnabled"); + }); +} diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts index 8d8279802..1b175f0c0 100644 --- a/backend/src/db/schemas/organizations.ts +++ b/backend/src/db/schemas/organizations.ts @@ -28,7 +28,11 @@ export const OrganizationsSchema = z.object({ privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(), privilegeUpgradeInitiatedAt: z.date().nullable().optional(), bypassOrgAuthEnabled: z.boolean().default(false), - userTokenExpiration: z.string().nullable().optional() + userTokenExpiration: z.string().nullable().optional(), + secretsProductEnabled: z.boolean().default(true).nullable().optional(), + pkiProductEnabled: z.boolean().default(true).nullable().optional(), + kmsProductEnabled: z.boolean().default(true).nullable().optional(), + sshProductEnabled: z.boolean().default(true).nullable().optional() }); export type TOrganizations = z.infer; diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index 018e457fa..6fa6e4d54 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -110,7 +110,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await pkiRouter.register(registerPkiCollectionRouter, { prefix: "/collections" }); await pkiRouter.register(registerPkiSubscriberRouter, { prefix: "/subscribers" }); }, - { prefix: "/pki" } + { prefix: "/pki" } // TODO(andrey): Block this if PKI PRODUCT disabled? ); await server.register(registerProjectBotRouter, { prefix: "/bot" }); @@ -129,7 +129,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" }); await server.register(registerDashboardRouter, { prefix: "/dashboard" }); - await server.register(registerCmekRouter, { prefix: "/kms" }); + await server.register(registerCmekRouter, { prefix: "/kms" }); // TODO(andrey): Block this if KMS PRODUCT disabled? await server.register(registerExternalGroupOrgRoleMappingRouter, { prefix: "/external-group-mappings" }); await server.register( diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index da1a251ff..991387d15 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -275,7 +275,11 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }, { message: "Duration value must be at least 1" } ) - .optional() + .optional(), + secretsProductEnabled: z.boolean().optional(), + pkiProductEnabled: z.boolean().optional(), + kmsProductEnabled: z.boolean().optional(), + sshProductEnabled: z.boolean().optional() }), response: { 200: z.object({ @@ -286,6 +290,9 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { + console.log("REQ BODY"); + console.log(req.body); + const organization = await server.services.org.updateOrg({ actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/server/routes/v3/index.ts b/backend/src/server/routes/v3/index.ts index ed8401560..b0656f1f1 100644 --- a/backend/src/server/routes/v3/index.ts +++ b/backend/src/server/routes/v3/index.ts @@ -9,7 +9,7 @@ export const registerV3Routes = async (server: FastifyZodProvider) => { await server.register(registerSignupRouter, { prefix: "/signup" }); await server.register(registerLoginRouter, { prefix: "/auth" }); await server.register(registerUserRouter, { prefix: "/users" }); - await server.register(registerSecretRouter, { prefix: "/secrets" }); + await server.register(registerSecretRouter, { prefix: "/secrets" }); // TODO(andrey): Block this if SECRETS PRODUCT disabled? await server.register(registerSecretBlindIndexRouter, { prefix: "/workspaces" }); await server.register(registerExternalMigrationRouter, { prefix: "/migrate" }); }; diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts index 5a1a4c333..df5b86d5c 100644 --- a/backend/src/services/org/org-schema.ts +++ b/backend/src/services/org/org-schema.ts @@ -18,5 +18,9 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({ privilegeUpgradeInitiatedByUsername: true, privilegeUpgradeInitiatedAt: true, bypassOrgAuthEnabled: true, - userTokenExpiration: true + userTokenExpiration: true, + secretsProductEnabled: true, + pkiProductEnabled: true, + kmsProductEnabled: true, + sshProductEnabled: true }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index d794391c1..7fb244c36 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -355,7 +355,11 @@ export const orgServiceFactory = ({ selectedMfaMethod, allowSecretSharingOutsideOrganization, bypassOrgAuthEnabled, - userTokenExpiration + userTokenExpiration, + secretsProductEnabled, + pkiProductEnabled, + kmsProductEnabled, + sshProductEnabled } }: TUpdateOrgDTO) => { const appCfg = getConfig(); @@ -457,7 +461,11 @@ export const orgServiceFactory = ({ selectedMfaMethod, allowSecretSharingOutsideOrganization, bypassOrgAuthEnabled, - userTokenExpiration + userTokenExpiration, + secretsProductEnabled, + pkiProductEnabled, + kmsProductEnabled, + sshProductEnabled }); if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); return org; diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts index 702cd25bf..ea336d146 100644 --- a/backend/src/services/org/org-types.ts +++ b/backend/src/services/org/org-types.ts @@ -75,6 +75,10 @@ export type TUpdateOrgDTO = { allowSecretSharingOutsideOrganization: boolean; bypassOrgAuthEnabled: boolean; userTokenExpiration: string; + secretsProductEnabled: boolean; + pkiProductEnabled: boolean; + kmsProductEnabled: boolean; + sshProductEnabled: boolean; }>; } & TOrgPermission; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 06125b1d7..737cc6bd3 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -112,7 +112,11 @@ export const useUpdateOrg = () => { selectedMfaMethod, allowSecretSharingOutsideOrganization, bypassOrgAuthEnabled, - userTokenExpiration + userTokenExpiration, + secretsProductEnabled, + pkiProductEnabled, + kmsProductEnabled, + sshProductEnabled }) => { return apiRequest.patch(`/api/v1/organization/${orgId}`, { name, @@ -124,7 +128,11 @@ export const useUpdateOrg = () => { selectedMfaMethod, allowSecretSharingOutsideOrganization, bypassOrgAuthEnabled, - userTokenExpiration + userTokenExpiration, + secretsProductEnabled, + pkiProductEnabled, + kmsProductEnabled, + sshProductEnabled }); }, onSuccess: () => { diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 6f63d003e..5da571504 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -20,6 +20,10 @@ export type Organization = { allowSecretSharingOutsideOrganization?: boolean; userTokenExpiration?: string; userRole: string; + secretsProductEnabled: boolean; + pkiProductEnabled: boolean; + kmsProductEnabled: boolean; + sshProductEnabled: boolean; }; export type UpdateOrgDTO = { @@ -34,6 +38,10 @@ export type UpdateOrgDTO = { allowSecretSharingOutsideOrganization?: boolean; bypassOrgAuthEnabled?: boolean; userTokenExpiration?: string; + secretsProductEnabled?: boolean; + pkiProductEnabled?: boolean; + kmsProductEnabled?: boolean; + sshProductEnabled?: boolean; }; export type BillingDetails = { diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index b85499073..b466c33d5 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -268,62 +268,70 @@ export const MinimizedOrgSidebar = () => {
- - {({ isActive }) => ( - - Secrets - - )} - - - {({ isActive }) => ( - - PKI - - )} - - - {({ isActive }) => ( - - KMS - - )} - - - {({ isActive }) => ( - - SSH - - )} - + {currentOrg.secretsProductEnabled && ( + + {({ isActive }) => ( + + Secrets + + )} + + )} + {currentOrg.pkiProductEnabled && ( + + {({ isActive }) => ( + + PKI + + )} + + )} + {currentOrg.kmsProductEnabled && ( + + {({ isActive }) => ( + + KMS + + )} + + )} + {currentOrg.sshProductEnabled && ( + + {({ isActive }) => ( + + SSH + + )} + + )}
{({ isActive }) => ( diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx index 917d08be9..85fdfa132 100644 --- a/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/OrgGeneralTab/OrgGeneralTab.tsx @@ -3,14 +3,18 @@ import { useOrgPermission } from "@app/context"; import { OrgDeleteSection } from "../OrgDeleteSection"; import { OrgIncidentContactsSection } from "../OrgIncidentContactsSection"; import { OrgNameChangeSection } from "../OrgNameChangeSection"; +import { OrgProductSelectSection } from "../OrgProductSelectSection"; export const OrgGeneralTab = () => { const { membership } = useOrgPermission(); return ( -
- - - {membership && membership.role === "admin" && } -
+ <> +
+ + + {membership && membership.role === "admin" && } +
+ + ); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx new file mode 100644 index 000000000..6f303b05e --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/OrgProductSelectSection.tsx @@ -0,0 +1,84 @@ +import { createNotification } from "@app/components/notifications"; +import { Switch } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useUpdateOrg } from "@app/hooks/api"; +import { useEffect, useState } from "react"; + +export const OrgProductSelectSection = () => { + const [toggledProducts, setToggledProducts] = useState<{ + [key: string]: { name: string; enabled: boolean }; + }>({ + secretsProductEnabled: { + name: "Secrets", + enabled: true + }, + pkiProductEnabled: { + name: "PKI", + enabled: true + }, + kmsProductEnabled: { + name: "KMS", + enabled: true + }, + sshProductEnabled: { + name: "SSH", + enabled: true + } + }); + + const { currentOrg } = useOrganization(); + const { mutateAsync } = useUpdateOrg(); + + useEffect(() => { + for (const [key, value] of Object.entries(currentOrg)) { + if (key in toggledProducts && typeof value === "boolean") { + setToggledProducts((products) => ({ + ...products, + [key]: { ...products[key], enabled: value } + })); + } + } + }, [currentOrg]); + + const onProductToggle = async (value: boolean, key: string) => { + setToggledProducts((products) => ({ + ...products, + [key]: { ...products[key], enabled: value } + })); + + console.log(key, value); + + // Update backend + await mutateAsync({ + orgId: currentOrg.id, + [key]: value + }); + + createNotification({ + text: `Successfully ${value ? "enabled" : "disabled"} ${toggledProducts[key].name}`, + type: "success" + }); + }; + + return ( +
+

Organization Products

+

+ Select which products are available for your organization. +

+ +
+ {Object.entries(toggledProducts).map(([key, product]) => ( + onProductToggle(value, key)} + isChecked={product.enabled} + > +

{product.name}

+
+ ))} +
+
+ ); +}; diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/index.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/index.tsx new file mode 100644 index 000000000..fb1bdbb29 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/OrgProductSelectSection/index.tsx @@ -0,0 +1 @@ +export { OrgProductSelectSection } from "./OrgProductSelectSection";