diff --git a/backend/src/ee/services/license/licence-enums.ts b/backend/src/ee/services/license/licence-enums.ts new file mode 100644 index 000000000..047eb0a38 --- /dev/null +++ b/backend/src/ee/services/license/licence-enums.ts @@ -0,0 +1,24 @@ +export const BillingPlanRows = { + MemberLimit: { name: "Organization member limit", field: "memberLimit" }, + IdentityLimit: { name: "Organization identity limit", field: "identityLimit" }, + WorkspaceLimit: { name: "Project limit", field: "workspaceLimit" }, + EnvironmentLimit: { name: "Environment limit", field: "environmentLimit" }, + SecretVersioning: { name: "Secret versioning", field: "secretVersioning" }, + PitRecovery: { name: "Point in time recovery", field: "pitRecovery" }, + Rbac: { name: "RBAC", field: "rbac" }, + CustomRateLimits: { name: "Custom rate limits", field: "customRateLimits" }, + CustomAlerts: { name: "Custom alerts", field: "customAlerts" }, + AuditLogs: { name: "Audit logs", field: "auditLogs" }, + SamlSSO: { name: "SAML SSO", field: "samlSSO" }, + Hsm: { name: "Hardware Security Module (HSM)", field: "hsm" }, + OidcSSO: { name: "OIDC SSO", field: "oidcSSO" }, + SecretApproval: { name: "Secret approvals", field: "secretApproval" }, + SecretRotation: { name: "Secret rotation", field: "secretRotation" }, + InstanceUserManagement: { name: "Instance User Management", field: "instanceUserManagement" }, + ExternalKms: { name: "External KMS", field: "externalKms" } +} as const; + +export const BillingPlanTableHead = { + Allowed: { name: "Allowed" }, + Used: { name: "Used" } +} as const; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index 7835ccfae..29c36c7fe 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -12,10 +12,13 @@ import { getConfig } from "@app/lib/config/env"; import { verifyOfflineLicense } from "@app/lib/crypto"; import { NotFoundError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; +import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal"; import { TOrgDALFactory } from "@app/services/org/org-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { TPermissionServiceFactory } from "../permission/permission-service"; +import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums"; import { TLicenseDALFactory } from "./license-dal"; import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns"; import { @@ -28,6 +31,7 @@ import { TFeatureSet, TGetOrgBillInfoDTO, TGetOrgTaxIdDTO, + TOfflineLicense, TOfflineLicenseContents, TOrgInvoiceDTO, TOrgLicensesDTO, @@ -39,10 +43,12 @@ import { } from "./license-types"; type TLicenseServiceFactoryDep = { - orgDAL: Pick; + orgDAL: Pick; permissionService: Pick; licenseDAL: TLicenseDALFactory; keyStore: Pick; + identityOrgMembershipDAL: TIdentityOrgDALFactory; + projectDAL: TProjectDALFactory; }; export type TLicenseServiceFactory = ReturnType; @@ -57,11 +63,14 @@ export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL, - keyStore + keyStore, + identityOrgMembershipDAL, + projectDAL }: TLicenseServiceFactoryDep) => { let isValidLicense = false; let instanceType = InstanceType.OnPrem; let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures(); + let selfHostedLicense: TOfflineLicense | null = null; const appCfg = getConfig(); const licenseServerCloudApi = setupLicenseRequestWithStore( @@ -125,6 +134,7 @@ export const licenseServiceFactory = ({ instanceType = InstanceType.EnterpriseOnPremOffline; logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`); isValidLicense = true; + selfHostedLicense = contents.license; return; } } @@ -348,10 +358,21 @@ export const licenseServiceFactory = ({ message: `Organization with ID '${orgId}' not found` }); } - const { data } = await licenseServerCloudApi.request.get( - `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` - ); - return data; + if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { + const { data } = await licenseServerCloudApi.request.get( + `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing` + ); + return data; + } + + return { + currentPeriodStart: selfHostedLicense?.issuedAt ? Date.parse(selfHostedLicense?.issuedAt) / 1000 : undefined, + currentPeriodEnd: selfHostedLicense?.expiresAt ? Date.parse(selfHostedLicense?.expiresAt) / 1000 : undefined, + interval: "month", + intervalCount: 1, + amount: 0, + quantity: 1 + }; }; // returns org current plan feature table @@ -365,10 +386,41 @@ export const licenseServiceFactory = ({ message: `Organization with ID '${orgId}' not found` }); } - const { data } = await licenseServerCloudApi.request.get( - `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` + if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) { + const { data } = await licenseServerCloudApi.request.get( + `/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table` + ); + return data; + } + + const mappedRows = await Promise.all( + Object.values(BillingPlanRows).map(async ({ name, field }: { name: string; field: string }) => { + const allowed = onPremFeatures[field as keyof TFeatureSet]; + let used = "-"; + + if (field === BillingPlanRows.MemberLimit.field) { + const orgMemberships = await orgDAL.countAllOrgMembers(orgId); + used = orgMemberships.toString(); + } else if (field === BillingPlanRows.WorkspaceLimit.field) { + const projects = await projectDAL.find({ orgId }); + used = projects.length.toString(); + } else if (field === BillingPlanRows.IdentityLimit.field) { + const identities = await identityOrgMembershipDAL.countAllOrgIdentities({ orgId }); + used = identities.toString(); + } + + return { + name, + allowed, + used + }; + }) ); - return data; + + return { + head: Object.values(BillingPlanTableHead), + rows: mappedRows + }; }; const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 9aa60a631..dac88791f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -413,7 +413,14 @@ export const registerRoutes = async ( serviceTokenDAL, projectDAL }); - const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore }); + const licenseService = licenseServiceFactory({ + permissionService, + orgDAL, + licenseDAL, + keyStore, + identityOrgMembershipDAL, + projectDAL + }); const hsmService = hsmServiceFactory({ hsmModule, diff --git a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx index e78d86703..054a873a3 100644 --- a/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/ProductsSideBar/DefaultSideBar.tsx @@ -12,17 +12,13 @@ export const DefaultSideBar = () => ( )} - {(window.location.origin.includes("https://app.infisical.com") || - window.location.origin.includes("https://eu.infisical.com") || - window.location.origin.includes("https://gamma.infisical.com")) && ( - - {({ isActive }) => ( - - Usage & Billing - - )} - - )} + + {({ isActive }) => ( + + Usage & Billing + + )} + diff --git a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx index 3cc104f48..f625a1627 100644 --- a/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/MinimizedOrgSidebar/MinimizedOrgSidebar.tsx @@ -370,17 +370,11 @@ export const MinimizedOrgSidebar = () => { Gateways - {(window.location.origin.includes("https://app.infisical.com") || - window.location.origin.includes("https://eu.infisical.com") || - window.location.origin.includes("https://gamma.infisical.com")) && ( - - } - > - Usage & Billing - - - )} + + }> + Usage & Billing + + }> Audit Logs diff --git a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx index ab160e4c5..ac5f72bcc 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingCloudTab/PreviewSection.tsx @@ -50,10 +50,20 @@ export const PreviewSection = () => { return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " "); } + const isCloudInstance = + window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://eu.infisical.com") || + window.location.origin.includes("https://gamma.infisical.com"); + const handleUpgradeBtnClick = async () => { try { if (!subscription || !currentOrg) return; + if (!isCloudInstance) { + window.open("https://infisical.com/pricing", "_blank"); + return; + } + if (!subscription.has_used_trial) { // direct user to start pro trial const url = await getOrgTrialUrl.mutateAsync({ @@ -71,6 +81,19 @@ export const PreviewSection = () => { } }; + const getUpgradePlanLabel = () => { + if (!isCloudInstance) { + return ( +
+ Go to Pricing + +
+ ); + } + + return !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"; + }; + return (
{subscription && @@ -97,7 +120,7 @@ export const PreviewSection = () => { color="mineshaft" isDisabled={!isAllowed} > - {!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"} + {getUpgradePlanLabel()} )} @@ -133,22 +156,24 @@ export const PreviewSection = () => { subscription.status === "trialing" ? "(Trial)" : "" }`}

- - {(isAllowed) => ( - - )} - + {isCloudInstance && ( + + {(isAllowed) => ( + + )} + + )}

Price

@@ -161,7 +186,7 @@ export const PreviewSection = () => {

Subscription renews on

- {formatDate(data.currentPeriodEnd)} + {data.currentPeriodEnd ? formatDate(data.currentPeriodEnd) : "-"}

diff --git a/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx b/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx index 6d8b59d2a..a7fb41afe 100644 --- a/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx +++ b/frontend/src/pages/organization/BillingPage/components/BillingTabGroup/BillingTabGroup.tsx @@ -16,25 +16,38 @@ const tabs = [ export const BillingTabGroup = withPermission( () => { + const isCloudInstance = + window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://eu.infisical.com") || + window.location.origin.includes("https://gamma.infisical.com"); + + const tabsFiltered = isCloudInstance + ? tabs + : [{ name: "Infisical Self-Hosted", key: "tab-infisical-cloud" }]; + return ( - {tabs.map((tab) => ( + {tabsFiltered.map((tab) => ( {tab.name} ))} - - - - - - - - - + {isCloudInstance && ( + <> + + + + + + + + + + + )} ); },