mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 15:28:25 +00:00
Show usage and billing for self-hosted instances
This commit is contained in:
@@ -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;
|
||||||
@@ -12,10 +12,13 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { verifyOfflineLicense } from "@app/lib/crypto";
|
import { verifyOfflineLicense } from "@app/lib/crypto";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { TIdentityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
import { TOrgDALFactory } from "@app/services/org/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 { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { BillingPlanRows, BillingPlanTableHead } from "./licence-enums";
|
||||||
import { TLicenseDALFactory } from "./license-dal";
|
import { TLicenseDALFactory } from "./license-dal";
|
||||||
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";
|
import { getDefaultOnPremFeatures, setupLicenseRequestWithStore } from "./license-fns";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +31,7 @@ import {
|
|||||||
TFeatureSet,
|
TFeatureSet,
|
||||||
TGetOrgBillInfoDTO,
|
TGetOrgBillInfoDTO,
|
||||||
TGetOrgTaxIdDTO,
|
TGetOrgTaxIdDTO,
|
||||||
|
TOfflineLicense,
|
||||||
TOfflineLicenseContents,
|
TOfflineLicenseContents,
|
||||||
TOrgInvoiceDTO,
|
TOrgInvoiceDTO,
|
||||||
TOrgLicensesDTO,
|
TOrgLicensesDTO,
|
||||||
@@ -39,10 +43,12 @@ import {
|
|||||||
} from "./license-types";
|
} from "./license-types";
|
||||||
|
|
||||||
type TLicenseServiceFactoryDep = {
|
type TLicenseServiceFactoryDep = {
|
||||||
orgDAL: Pick<TOrgDALFactory, "findOrgById">;
|
orgDAL: Pick<TOrgDALFactory, "findOrgById" | "countAllOrgMembers">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseDAL: TLicenseDALFactory;
|
licenseDAL: TLicenseDALFactory;
|
||||||
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem">;
|
||||||
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||||
@@ -57,11 +63,14 @@ export const licenseServiceFactory = ({
|
|||||||
orgDAL,
|
orgDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseDAL,
|
licenseDAL,
|
||||||
keyStore
|
keyStore,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
projectDAL
|
||||||
}: TLicenseServiceFactoryDep) => {
|
}: TLicenseServiceFactoryDep) => {
|
||||||
let isValidLicense = false;
|
let isValidLicense = false;
|
||||||
let instanceType = InstanceType.OnPrem;
|
let instanceType = InstanceType.OnPrem;
|
||||||
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
||||||
|
let selfHostedLicense: TOfflineLicense | null = null;
|
||||||
|
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const licenseServerCloudApi = setupLicenseRequestWithStore(
|
const licenseServerCloudApi = setupLicenseRequestWithStore(
|
||||||
@@ -125,6 +134,7 @@ export const licenseServiceFactory = ({
|
|||||||
instanceType = InstanceType.EnterpriseOnPremOffline;
|
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||||
isValidLicense = true;
|
isValidLicense = true;
|
||||||
|
selfHostedLicense = contents.license;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,10 +358,21 @@ export const licenseServiceFactory = ({
|
|||||||
message: `Organization with ID '${orgId}' not found`
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) {
|
||||||
const { data } = await licenseServerCloudApi.request.get(
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||||
);
|
);
|
||||||
return data;
|
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
|
// returns org current plan feature table
|
||||||
@@ -365,10 +386,41 @@ export const licenseServiceFactory = ({
|
|||||||
message: `Organization with ID '${orgId}' not found`
|
message: `Organization with ID '${orgId}' not found`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (instanceType !== InstanceType.OnPrem && instanceType !== InstanceType.EnterpriseOnPremOffline) {
|
||||||
const { data } = await licenseServerCloudApi.request.get(
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
|
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
|
||||||
);
|
);
|
||||||
return data;
|
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 {
|
||||||
|
head: Object.values(BillingPlanTableHead),
|
||||||
|
rows: mappedRows
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TGetOrgBillInfoDTO) => {
|
||||||
|
|||||||
@@ -413,7 +413,14 @@ export const registerRoutes = async (
|
|||||||
serviceTokenDAL,
|
serviceTokenDAL,
|
||||||
projectDAL
|
projectDAL
|
||||||
});
|
});
|
||||||
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL, keyStore });
|
const licenseService = licenseServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
orgDAL,
|
||||||
|
licenseDAL,
|
||||||
|
keyStore,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
projectDAL
|
||||||
|
});
|
||||||
|
|
||||||
const hsmService = hsmServiceFactory({
|
const hsmService = hsmServiceFactory({
|
||||||
hsmModule,
|
hsmModule,
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ export const DefaultSideBar = () => (
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
|
||||||
window.location.origin.includes("https://eu.infisical.com") ||
|
|
||||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
|
||||||
<Link to="/organization/billing">
|
<Link to="/organization/billing">
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuItem isSelected={isActive} icon="spinning-coin">
|
<MenuItem isSelected={isActive} icon="spinning-coin">
|
||||||
@@ -22,7 +19,6 @@ export const DefaultSideBar = () => (
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
</MenuGroup>
|
</MenuGroup>
|
||||||
<MenuGroup title="Other">
|
<MenuGroup title="Other">
|
||||||
<Link to="/organization/access-management">
|
<Link to="/organization/access-management">
|
||||||
|
|||||||
+1
-7
@@ -370,17 +370,11 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
Gateways
|
Gateways
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
|
||||||
window.location.origin.includes("https://eu.infisical.com") ||
|
|
||||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
|
||||||
<Link to="/organization/billing">
|
<Link to="/organization/billing">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faMoneyBill} />}>
|
||||||
icon={<FontAwesomeIcon className="w-3" icon={faMoneyBill} />}
|
|
||||||
>
|
|
||||||
Usage & Billing
|
Usage & Billing
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
<Link to="/organization/audit-logs">
|
<Link to="/organization/audit-logs">
|
||||||
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faBook} />}>
|
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faBook} />}>
|
||||||
Audit Logs
|
Audit Logs
|
||||||
|
|||||||
+27
-2
@@ -50,10 +50,20 @@ export const PreviewSection = () => {
|
|||||||
return slug.replace(/(\b[a-z])/g, (match) => match.toUpperCase()).replace(/-/g, " ");
|
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 () => {
|
const handleUpgradeBtnClick = async () => {
|
||||||
try {
|
try {
|
||||||
if (!subscription || !currentOrg) return;
|
if (!subscription || !currentOrg) return;
|
||||||
|
|
||||||
|
if (!isCloudInstance) {
|
||||||
|
window.open("https://infisical.com/pricing", "_blank");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!subscription.has_used_trial) {
|
if (!subscription.has_used_trial) {
|
||||||
// direct user to start pro trial
|
// direct user to start pro trial
|
||||||
const url = await getOrgTrialUrl.mutateAsync({
|
const url = await getOrgTrialUrl.mutateAsync({
|
||||||
@@ -71,6 +81,19 @@ export const PreviewSection = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUpgradePlanLabel = () => {
|
||||||
|
if (!isCloudInstance) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Go to Pricing
|
||||||
|
<FontAwesomeIcon icon={faArrowUpRightFromSquare} className="mb-[0.06rem] ml-1 text-xs" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{subscription &&
|
{subscription &&
|
||||||
@@ -97,7 +120,7 @@ export const PreviewSection = () => {
|
|||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
>
|
>
|
||||||
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
{getUpgradePlanLabel()}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
@@ -133,6 +156,7 @@ export const PreviewSection = () => {
|
|||||||
subscription.status === "trialing" ? "(Trial)" : ""
|
subscription.status === "trialing" ? "(Trial)" : ""
|
||||||
}`}
|
}`}
|
||||||
</p>
|
</p>
|
||||||
|
{isCloudInstance && (
|
||||||
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Billing}>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<button
|
<button
|
||||||
@@ -149,6 +173,7 @@ export const PreviewSection = () => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</OrgPermissionCan>
|
</OrgPermissionCan>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mr-4 flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<p className="mb-2 text-gray-400">Price</p>
|
<p className="mb-2 text-gray-400">Price</p>
|
||||||
@@ -161,7 +186,7 @@ export const PreviewSection = () => {
|
|||||||
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="flex-1 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||||
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
|
<p className="mb-8 text-2xl font-semibold text-mineshaft-50">
|
||||||
{formatDate(data.currentPeriodEnd)}
|
{data.currentPeriodEnd ? formatDate(data.currentPeriodEnd) : "-"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+14
-1
@@ -16,16 +16,27 @@ const tabs = [
|
|||||||
|
|
||||||
export const BillingTabGroup = withPermission(
|
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 (
|
return (
|
||||||
<Tabs defaultValue={tabs[0].key}>
|
<Tabs defaultValue={tabs[0].key}>
|
||||||
<TabList>
|
<TabList>
|
||||||
{tabs.map((tab) => (
|
{tabsFiltered.map((tab) => (
|
||||||
<Tab value={tab.key}>{tab.name}</Tab>
|
<Tab value={tab.key}>{tab.name}</Tab>
|
||||||
))}
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={tabs[0].key}>
|
<TabPanel value={tabs[0].key}>
|
||||||
<BillingCloudTab />
|
<BillingCloudTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
{isCloudInstance && (
|
||||||
|
<>
|
||||||
<TabPanel value={tabs[1].key}>
|
<TabPanel value={tabs[1].key}>
|
||||||
<BillingSelfHostedTab />
|
<BillingSelfHostedTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
@@ -35,6 +46,8 @@ export const BillingTabGroup = withPermission(
|
|||||||
<TabPanel value={tabs[3].key}>
|
<TabPanel value={tabs[3].key}>
|
||||||
<BillingDetailsTab />
|
<BillingDetailsTab />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user