diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index d69f7bf95..3e30276cb 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -38,7 +38,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ has_used_trial: true, secretApproval: false, secretRotation: true, - caCrl: false + caCrl: false, + instanceUserManagement: false }); export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 36b03ff80..72ea25e91 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -56,6 +56,7 @@ export type TFeatureSet = { secretApproval: false; secretRotation: true; caCrl: false; + instanceUserManagement: false; }; export type TOrgPlansTableDTO = { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index e0befd7a4..c845de988 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -469,7 +469,8 @@ export const registerRoutes = async ( authService: loginService, serverCfgDAL: superAdminDAL, orgService, - keyStore + keyStore, + licenseService }); const rateLimitService = rateLimitServiceFactory({ rateLimitDAL, diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 650910681..e7798500f 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -1,6 +1,7 @@ import bcrypt from "bcrypt"; import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TKeyStoreFactory } from "@app/keystore/keystore"; import { getConfig } from "@app/lib/config/env"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; @@ -20,6 +21,7 @@ type TSuperAdminServiceFactoryDep = { authService: Pick; orgService: Pick; keyStore: Pick; + licenseService: Pick; }; export type TSuperAdminServiceFactory = ReturnType; @@ -36,7 +38,8 @@ export const superAdminServiceFactory = ({ userDAL, authService, orgService, - keyStore + keyStore, + licenseService }: TSuperAdminServiceFactoryDep) => { const initServerCfg = async () => { // TODO(akhilmhdh): bad pattern time less change this later to me itself @@ -219,6 +222,12 @@ export const superAdminServiceFactory = ({ }; const deleteUser = async (userId: string) => { + if (!licenseService.onPremFeatures?.instanceUserManagement) { + throw new BadRequestError({ + message: "Failed to delete user due to plan restriction. Upgrade to Infisical's Pro plan." + }); + } + const user = await userDAL.deleteById(userId); return user; }; diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 89635a953..6a8b053b4 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -39,4 +39,5 @@ export type SubscriptionPlan = { trial_end: number | null; has_used_trial: boolean; caCrl: boolean; + instanceUserManagement: boolean; }; diff --git a/frontend/src/views/admin/DashboardPage/UserPanel.tsx b/frontend/src/views/admin/DashboardPage/UserPanel.tsx index 733d52624..2475ef14b 100644 --- a/frontend/src/views/admin/DashboardPage/UserPanel.tsx +++ b/frontend/src/views/admin/DashboardPage/UserPanel.tsx @@ -16,9 +16,10 @@ import { Td, Th, THead, - Tr + Tr, + UpgradePlanModal } from "@app/components/v2"; -import { useUser } from "@app/context"; +import { useSubscription, useUser } from "@app/context"; import { useDebounce, usePopUp } from "@app/hooks"; import { useAdminDeleteUser, useAdminGetUsers } from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; @@ -27,8 +28,8 @@ const UserPanelTable = ({ handlePopUpOpen }: { handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["removeUser"]>, - data: { + popUpName: keyof UsePopUpState<["removeUser", "upgradePlan"]>, + data?: { username: string; id: string; } @@ -38,6 +39,7 @@ const UserPanelTable = ({ const { user } = useUser(); const userId = user?.id || ""; const debounedSearchTerm = useDebounce(searchUserFilter, 500); + const { subscription } = useSubscription(); const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ limit: 20, @@ -83,7 +85,13 @@ const UserPanelTable = ({ variant="plain" ariaLabel="update" isDisabled={userId === id} - onClick={() => handlePopUpOpen("removeUser", { username, id })} + onClick={() => { + if (!subscription?.instanceUserManagement) { + handlePopUpOpen("upgradePlan"); + return; + } + handlePopUpOpen("removeUser", { username, id }); + }} > @@ -117,7 +125,8 @@ const UserPanelTable = ({ export const UserPanel = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - "removeUser" + "removeUser", + "upgradePlan" ] as const); const { mutateAsync: deleteUser } = useAdminDeleteUser(); @@ -156,6 +165,11 @@ export const UserPanel = () => { onChange={(isOpen) => handlePopUpToggle("removeUser", isOpen)} onDeleteApproved={handleRemoveUser} /> + handlePopUpToggle("upgradePlan", isOpen)} + text="Deleting users via Admin UI is only available on Infisical's Pro plan and above." + /> ); };