diff --git a/messages/en-US.json b/messages/en-US.json index 0129c1159..4701d4da1 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1915,6 +1915,9 @@ "billingDomains": "Domains", "billingOrganizations": "Orgs", "billingRemoteExitNodes": "Remote Nodes", + "billingPublicResources": "Public Resources", + "billingPrivateResources": "Private Resources", + "billingMachineClients": "Machine Clients", "billingNoLimitConfigured": "No limit configured", "billingEstimatedPeriod": "Estimated Billing Period", "billingIncludedUsage": "Included Usage", @@ -1943,6 +1946,9 @@ "billingUsersInfo": "How many users you can use", "billingDomainInfo": "How many domains you can use", "billingRemoteExitNodesInfo": "How many remote nodes you can use", + "billingPublicResourcesInfo": "How many public resources you can use", + "billingPrivateResourcesInfo": "How many private resources you can use", + "billingMachineClientsInfo": "How many machine clients you can use", "billingLicenseKeys": "License Keys", "billingLicenseKeysDescription": "Manage your license key subscriptions", "billingLicenseSubscription": "License Subscription", diff --git a/server/lib/billing/features.ts b/server/lib/billing/features.ts index eff042ebf..ffe24a4a3 100644 --- a/server/lib/billing/features.ts +++ b/server/lib/billing/features.ts @@ -27,6 +27,12 @@ export async function getFeatureDisplayName( return "Remote Exit Nodes"; case LimitId.ORGANIZATIONS: return "Organizations"; + case LimitId.PUBLIC_RESOURCES: + return "Public Resources"; + case LimitId.PRIVATE_RESOURCES: + return "Private Resources"; + case LimitId.MACHINE_CLIENTS: + return "Machine Clients"; case LimitId.TIER1: return "Home Lab"; default: diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts index 6b61b84de..1910a6a33 100644 --- a/server/private/routers/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -104,6 +104,18 @@ export async function getOrgUsage( orgId, LimitId.ORGANIZATIONS ); + const publicResources = await usageService.getUsage( + orgId, + LimitId.PUBLIC_RESOURCES + ); + const privateResources = await usageService.getUsage( + orgId, + LimitId.PRIVATE_RESOURCES + ); + const machineClients = await usageService.getUsage( + orgId, + LimitId.MACHINE_CLIENTS + ); // const egressData = await usageService.getUsage( // orgId, // FeatureId.EGRESS_DATA_MB @@ -127,6 +139,15 @@ export async function getOrgUsage( if (organizations) { usageData.push(organizations); } + if (publicResources) { + usageData.push(publicResources); + } + if (privateResources) { + usageData.push(privateResources); + } + if (machineClients) { + usageData.push(machineClients); + } const orgLimits = await db .select() diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index d526a17f4..b3d1bb351 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -158,6 +158,9 @@ const tierLimits: Record< domains: number; remoteNodes: number; organizations: number; + publicResources: number; + privateResources: number; + machineClients: number; } > = { basic: { @@ -165,35 +168,50 @@ const tierLimits: Record< sites: freeLimitSet[LimitId.SITES]?.value ?? 0, domains: freeLimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: freeLimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: freeLimitSet[LimitId.ORGANIZATIONS]?.value ?? 0 + organizations: freeLimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, + publicResources: freeLimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, + privateResources: freeLimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, + machineClients: freeLimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, tier1: { users: tier1LimitSet[LimitId.USERS]?.value ?? 0, sites: tier1LimitSet[LimitId.SITES]?.value ?? 0, domains: tier1LimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: tier1LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: tier1LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0 + organizations: tier1LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, + publicResources: tier1LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, + privateResources: tier1LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, + machineClients: tier1LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, tier2: { users: tier2LimitSet[LimitId.USERS]?.value ?? 0, sites: tier2LimitSet[LimitId.SITES]?.value ?? 0, domains: tier2LimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: tier2LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: tier2LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0 + organizations: tier2LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, + publicResources: tier2LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, + privateResources: tier2LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, + machineClients: tier2LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, tier3: { users: tier3LimitSet[LimitId.USERS]?.value ?? 0, sites: tier3LimitSet[LimitId.SITES]?.value ?? 0, domains: tier3LimitSet[LimitId.DOMAINS]?.value ?? 0, remoteNodes: tier3LimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: tier3LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0 + organizations: tier3LimitSet[LimitId.ORGANIZATIONS]?.value ?? 0, + publicResources: tier3LimitSet[LimitId.PUBLIC_RESOURCES]?.value ?? 0, + privateResources: tier3LimitSet[LimitId.PRIVATE_RESOURCES]?.value ?? 0, + machineClients: tier3LimitSet[LimitId.MACHINE_CLIENTS]?.value ?? 0 }, enterprise: { users: 0, // Custom for enterprise sites: 0, // Custom for enterprise domains: 0, // Custom for enterprise remoteNodes: 0, // Custom for enterprise - organizations: 0 // Custom for enterprise + organizations: 0, // Custom for enterprise + publicResources: 0, // Custom for enterprise + privateResources: 0, // Custom for enterprise + machineClients: 0 // Custom for enterprise } }; @@ -234,6 +252,9 @@ export default function BillingPage() { const DOMAINS = "domains"; const REMOTE_EXIT_NODES = "remoteExitNodes"; const ORGINIZATIONS = "organizations"; + const PUBLIC_RESOURCES = "publicResources"; + const PRIVATE_RESOURCES = "privateResources"; + const MACHINE_CLIENTS = "machineClients"; // Confirmation dialog state const [showConfirmDialog, setShowConfirmDialog] = useState(false); @@ -797,6 +818,45 @@ export default function BillingPage() { }); } + // Check public resources + const publicResourcesUsage = getUsageValue(PUBLIC_RESOURCES); + if ( + limits.publicResources > 0 && + publicResourcesUsage > limits.publicResources + ) { + violations.push({ + feature: "Public Resources", + currentUsage: publicResourcesUsage, + newLimit: limits.publicResources + }); + } + + // Check private resources + const privateResourcesUsage = getUsageValue(PRIVATE_RESOURCES); + if ( + limits.privateResources > 0 && + privateResourcesUsage > limits.privateResources + ) { + violations.push({ + feature: "Private Resources", + currentUsage: privateResourcesUsage, + newLimit: limits.privateResources + }); + } + + // Check machine clients + const machineClientsUsage = getUsageValue(MACHINE_CLIENTS); + if ( + limits.machineClients > 0 && + machineClientsUsage > limits.machineClients + ) { + violations.push({ + feature: "Machine Clients", + currentUsage: machineClientsUsage, + newLimit: limits.machineClients + }); + } + return violations; }; @@ -1025,7 +1085,7 @@ export default function BillingPage() {
{t("billingMaximumLimits") || "Maximum Limits"}
- + {t("billingUsers") || "Users"} @@ -1308,6 +1368,168 @@ export default function BillingPage() { )} + + + {t("billingPublicResources") || + "Public Resources"} + + + {isOverLimit(PUBLIC_RESOURCES) ? ( + + + + + {getLimitValue( + PUBLIC_RESOURCES + ) ?? + t( + "billingUnlimited" + ) ?? + "∞"} + + + +

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + PUBLIC_RESOURCES + ), + limit: + getLimitValue( + PUBLIC_RESOURCES + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(PUBLIC_RESOURCES)}) exceeds limit (${getLimitValue(PUBLIC_RESOURCES)})`} +

+
+
+ ) : ( + <> + {getLimitValue( + PUBLIC_RESOURCES + ) ?? + t("billingUnlimited") ?? + "∞"} + + )} +
+
+ + + {t("billingPrivateResources") || + "Private Resources"} + + + {isOverLimit(PRIVATE_RESOURCES) ? ( + + + + + {getLimitValue( + PRIVATE_RESOURCES + ) ?? + t( + "billingUnlimited" + ) ?? + "∞"} + + + +

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + PRIVATE_RESOURCES + ), + limit: + getLimitValue( + PRIVATE_RESOURCES + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(PRIVATE_RESOURCES)}) exceeds limit (${getLimitValue(PRIVATE_RESOURCES)})`} +

+
+
+ ) : ( + <> + {getLimitValue( + PRIVATE_RESOURCES + ) ?? + t("billingUnlimited") ?? + "∞"} + + )} +
+
+ + + {t("billingMachineClients") || + "Machine Clients"} + + + {isOverLimit(MACHINE_CLIENTS) ? ( + + + + + {getLimitValue( + MACHINE_CLIENTS + ) ?? + t( + "billingUnlimited" + ) ?? + "∞"} + + + +

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + MACHINE_CLIENTS + ), + limit: + getLimitValue( + MACHINE_CLIENTS + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(MACHINE_CLIENTS)}) exceeds limit (${getLimitValue(MACHINE_CLIENTS)})`} +

+
+
+ ) : ( + <> + {getLimitValue( + MACHINE_CLIENTS + ) ?? + t("billingUnlimited") ?? + "∞"} + + )} +
+
@@ -1507,6 +1729,45 @@ export default function BillingPage() { "Remote Nodes"} +
+ + + { + tierLimits[ + pendingTier.tier + ].publicResources + }{" "} + {t( + "billingPublicResources" + ) || "Public Resources"} + +
+
+ + + { + tierLimits[ + pendingTier.tier + ].privateResources + }{" "} + {t( + "billingPrivateResources" + ) || "Private Resources"} + +
+
+ + + { + tierLimits[ + pendingTier.tier + ].machineClients + }{" "} + {t( + "billingMachineClients" + ) || "Machine Clients"} + +
)}