From afee158b95cadbc9a4f28394e586d9a908585f22 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Wed, 19 Jun 2024 10:31:58 -0700 Subject: [PATCH] Start adding identity based pricing logic --- .../services/license/__mocks__/licence-fns.ts | 2 ++ .../src/ee/services/license/licence-fns.ts | 2 ++ .../ee/services/license/license-service.ts | 1 + .../src/ee/services/license/license-types.ts | 2 ++ backend/src/server/routes/index.ts | 3 +- .../src/services/identity/identity-service.ts | 2 ++ backend/src/services/org/org-service.ts | 13 ++++++-- docker-compose.dev.yml | 2 +- frontend/src/hooks/api/subscriptions/types.ts | 2 ++ frontend/src/layouts/AppLayout/AppLayout.tsx | 26 ++++++++-------- .../IdentitySection/IdentitySection.tsx | 30 ++++++++++++++++--- .../OrgMembersSection/OrgMembersSection.tsx | 18 ++++++----- 12 files changed, 74 insertions(+), 29 deletions(-) diff --git a/backend/src/ee/services/license/__mocks__/licence-fns.ts b/backend/src/ee/services/license/__mocks__/licence-fns.ts index ddbffba45..a8b3b351d 100644 --- a/backend/src/ee/services/license/__mocks__/licence-fns.ts +++ b/backend/src/ee/services/license/__mocks__/licence-fns.ts @@ -7,6 +7,8 @@ export const getDefaultOnPremFeatures = () => { workspacesUsed: 0, memberLimit: null, membersUsed: 0, + identityLimit: null, + identitiesUsed: 0, environmentLimit: null, environmentsUsed: 0, secretVersioning: true, diff --git a/backend/src/ee/services/license/licence-fns.ts b/backend/src/ee/services/license/licence-fns.ts index 9d2c5a472..45325d414 100644 --- a/backend/src/ee/services/license/licence-fns.ts +++ b/backend/src/ee/services/license/licence-fns.ts @@ -15,6 +15,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({ membersUsed: 0, environmentLimit: null, environmentsUsed: 0, + identityLimit: null, + identitiesUsed: 0, dynamicSecret: false, secretVersioning: true, pitRecovery: false, diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 46931468f..0819c461b 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -155,6 +155,7 @@ export const licenseServiceFactory = ({ LICENSE_SERVER_CLOUD_PLAN_TTL, JSON.stringify(currentPlan) ); + return currentPlan; } } catch (error) { diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index e23ff2c84..565387296 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -31,6 +31,8 @@ export type TFeatureSet = { dynamicSecret: false; memberLimit: null; membersUsed: 0; + identityLimit: null; + identitiesUsed: 0; environmentLimit: null; environmentsUsed: 0; secretVersioning: true; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 326fbafa6..f20a622ab 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -801,7 +801,8 @@ export const registerRoutes = async ( const identityService = identityServiceFactory({ permissionService, identityDAL, - identityOrgMembershipDAL + identityOrgMembershipDAL, + licenseService }); const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL, diff --git a/backend/src/services/identity/identity-service.ts b/backend/src/services/identity/identity-service.ts index 2863bf23e..f5db1e901 100644 --- a/backend/src/services/identity/identity-service.ts +++ b/backend/src/services/identity/identity-service.ts @@ -1,6 +1,7 @@ import { ForbiddenError } from "@casl/ability"; import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas"; +import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { isAtLeastAsPrivileged } from "@app/lib/casl"; @@ -16,6 +17,7 @@ type TIdentityServiceFactoryDep = { identityDAL: TIdentityDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory; permissionService: Pick; + licenseService: Pick; }; export type TIdentityServiceFactory = ReturnType; diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index 68d2b8cda..248bab568 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -420,13 +420,20 @@ export const orgServiceFactory = ({ } const plan = await licenseService.getPlan(orgId); - if (plan.memberLimit !== null && plan.membersUsed >= plan.memberLimit) { - // case: limit imposed on number of members allowed - // case: number of members used exceeds the number of members allowed + if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { + // limit imposed on number of members allowed / number of members used exceeds the number of members allowed throw new BadRequestError({ message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." }); } + + if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { + // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed + throw new BadRequestError({ + message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." + }); + } + const invitee = await orgDAL.transaction(async (tx) => { const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); if (inviteeUser) { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 422fe43f3..89e36e91a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -87,7 +87,7 @@ services: - 4000:4000 environment: - NODE_ENV=development - - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable + # - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable - TELEMETRY_ENABLED=false volumes: - ./backend/src:/app/src diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts index 66959ad1b..3d4a6dc36 100644 --- a/frontend/src/hooks/api/subscriptions/types.ts +++ b/frontend/src/hooks/api/subscriptions/types.ts @@ -2,6 +2,8 @@ export type SubscriptionPlan = { id: string; membersUsed: number; memberLimit: number; + identitiesUsed: number; + identityLimit: number; auditLogs: boolean; dynamicSecret: boolean; auditLogsRetentionDays: number; diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 72351df88..d4b1f17f4 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -652,19 +652,19 @@ export const AppLayout = ({ children }: LayoutProps) => { - {(window.location.origin.includes("https://app.infisical.com") || - window.location.origin.includes("https://gamma.infisical.com")) && ( - - - - Usage & Billing - - - - )} + {/* {(window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://gamma.infisical.com")) && ( */} + + + + Usage & Billing + + + + {/* )} */} { + const { subscription } = useSubscription(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id || ""; - const { mutateAsync: deleteMutateAsync } = useDeleteIdentity(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "identity", @@ -31,6 +36,10 @@ export const IdentitySection = withPermission( "upgradePlan" ] as const); + const isMoreIdentitiesAllowed = subscription?.identityLimit + ? subscription.identitiesUsed < subscription.identityLimit + : true; + const onDeleteIdentitySubmit = async (identityId: string) => { try { await deleteMutateAsync({ @@ -81,7 +90,15 @@ export const IdentitySection = withPermission( colorSchema="primary" type="submit" leftIcon={} - onClick={() => handlePopUpOpen("identity")} + onClick={() => { + if (!isMoreIdentitiesAllowed) { + handlePopUpOpen("upgradePlan", { + description: "You can add more identities if you upgrade your Infisical plan." + }); + return; + } + handlePopUpOpen("identity"); + }} isDisabled={!isAllowed} > Create identity @@ -118,6 +135,11 @@ export const IdentitySection = withPermission( ) } /> + handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> ); }, diff --git a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 17f32369c..8943f2940 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -23,7 +23,6 @@ import { AddOrgMemberModal } from "./AddOrgMemberModal"; import { OrgMembersTable } from "./OrgMembersTable"; export const OrgMembersSection = () => { - const { subscription } = useSubscription(); const { currentOrg } = useOrganization(); const orgId = currentOrg?.id ?? ""; @@ -39,9 +38,13 @@ export const OrgMembersSection = () => { const { mutateAsync: deleteMutateAsync } = useDeleteOrgMembership(); - const isMoreUsersNotAllowed = subscription?.memberLimit - ? subscription.membersUsed >= subscription.memberLimit - : false; + const isMoreUsersAllowed = subscription?.memberLimit + ? subscription.membersUsed < subscription.memberLimit + : true; + + const isMoreIdentitiesAllowed = subscription?.identityLimit + ? subscription.identitiesUsed < subscription.identityLimit + : true; const handleAddMemberModal = () => { if (currentOrg?.authEnforced) { @@ -52,13 +55,14 @@ export const OrgMembersSection = () => { return; } - if (isMoreUsersNotAllowed) { + if (!isMoreUsersAllowed || !isMoreIdentitiesAllowed) { handlePopUpOpen("upgradePlan", { description: "You can add more members if you upgrade your Infisical plan." }); - } else { - handlePopUpOpen("addMember"); + return; } + + handlePopUpOpen("addMember"); }; const onRemoveMemberSubmit = async (orgMembershipId: string) => {