diff --git a/server/lib/billing/features.ts b/server/lib/billing/features.ts index 6063b470f..9fea3da2c 100644 --- a/server/lib/billing/features.ts +++ b/server/lib/billing/features.ts @@ -1,4 +1,4 @@ -export enum FeatureId { +export enum LimitId { USERS = "users", SITES = "sites", EGRESS_DATA_MB = "egressDataMb", @@ -8,21 +8,23 @@ export enum FeatureId { TIER1 = "tier1" } -export async function getFeatureDisplayName(featureId: FeatureId): Promise { +export async function getFeatureDisplayName( + featureId: LimitId +): Promise { switch (featureId) { - case FeatureId.USERS: + case LimitId.USERS: return "Users"; - case FeatureId.SITES: + case LimitId.SITES: return "Sites"; - case FeatureId.EGRESS_DATA_MB: + case LimitId.EGRESS_DATA_MB: return "Egress Data (MB)"; - case FeatureId.DOMAINS: + case LimitId.DOMAINS: return "Domains"; - case FeatureId.REMOTE_EXIT_NODES: + case LimitId.REMOTE_EXIT_NODES: return "Remote Exit Nodes"; - case FeatureId.ORGINIZATIONS: + case LimitId.ORGINIZATIONS: return "Organizations"; - case FeatureId.TIER1: + case LimitId.TIER1: return "Home Lab"; default: return featureId; @@ -30,15 +32,16 @@ export async function getFeatureDisplayName(featureId: FeatureId): Promise> = { // right now we are not charging for any data +export const FeatureMeterIds: Partial> = { + // right now we are not charging for any data // [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW" }; -export const FeatureMeterIdsSandbox: Partial> = { +export const FeatureMeterIdsSandbox: Partial> = { // [FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ" }; -export function getFeatureMeterId(featureId: FeatureId): string | undefined { +export function getFeatureMeterId(featureId: LimitId): string | undefined { if ( process.env.ENVIRONMENT == "prod" && process.env.SANDBOX_MODE !== "true" @@ -49,22 +52,20 @@ export function getFeatureMeterId(featureId: FeatureId): string | undefined { } } -export function getFeatureIdByMetricId( - metricId: string -): FeatureId | undefined { - return (Object.entries(FeatureMeterIds) as [FeatureId, string][]).find( +export function getFeatureIdByMetricId(metricId: string): LimitId | undefined { + return (Object.entries(FeatureMeterIds) as [LimitId, string][]).find( ([_, v]) => v === metricId )?.[0]; } -export type FeaturePriceSet = Partial>; +export type FeaturePriceSet = Partial>; export const tier1FeaturePriceSet: FeaturePriceSet = { - [FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G" + [LimitId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G" }; export const tier1FeaturePriceSetSandbox: FeaturePriceSet = { - [FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT" + [LimitId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT" }; export function getTier1FeaturePriceSet(): FeaturePriceSet { @@ -79,11 +80,11 @@ export function getTier1FeaturePriceSet(): FeaturePriceSet { } export const tier2FeaturePriceSet: FeaturePriceSet = { - [FeatureId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN" + [LimitId.USERS]: "price_1SzVCcD3Ee2Ir7Wmn6U3KvPN" }; export const tier2FeaturePriceSetSandbox: FeaturePriceSet = { - [FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR" + [LimitId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR" }; export function getTier2FeaturePriceSet(): FeaturePriceSet { @@ -98,11 +99,11 @@ export function getTier2FeaturePriceSet(): FeaturePriceSet { } export const tier3FeaturePriceSet: FeaturePriceSet = { - [FeatureId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv" + [LimitId.USERS]: "price_1SzVDKD3Ee2Ir7WmPtOKNusv" }; export const tier3FeaturePriceSetSandbox: FeaturePriceSet = { - [FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs" + [LimitId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs" }; export function getTier3FeaturePriceSet(): FeaturePriceSet { @@ -116,7 +117,7 @@ export function getTier3FeaturePriceSet(): FeaturePriceSet { } } -export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined { +export function getFeatureIdByPriceId(priceId: string): LimitId | undefined { // Check all feature price sets const allPriceSets = [ getTier1FeaturePriceSet(), @@ -125,7 +126,7 @@ export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined { ]; for (const priceSet of allPriceSets) { - const entry = (Object.entries(priceSet) as [FeatureId, string][]).find( + const entry = (Object.entries(priceSet) as [LimitId, string][]).find( ([_, price]) => price === priceId ); if (entry) { diff --git a/server/lib/billing/getLineItems.ts b/server/lib/billing/getLineItems.ts index d386e5e96..5df5fb8a8 100644 --- a/server/lib/billing/getLineItems.ts +++ b/server/lib/billing/getLineItems.ts @@ -1,19 +1,19 @@ import Stripe from "stripe"; -import { FeatureId, FeaturePriceSet } from "./features"; +import { LimitId, FeaturePriceSet } from "./features"; import { usageService } from "./usageService"; export async function getLineItems( featurePriceSet: FeaturePriceSet, - orgId: string, + orgId: string ): Promise { - const users = await usageService.getUsage(orgId, FeatureId.USERS); + const users = await usageService.getUsage(orgId, LimitId.USERS); return Object.entries(featurePriceSet).map(([featureId, priceId]) => { let quantity: number | undefined; - if (featureId === FeatureId.USERS) { + if (featureId === LimitId.USERS) { quantity = users?.instantaneousValue || 1; - } else if (featureId === FeatureId.TIER1) { + } else if (featureId === LimitId.TIER1) { quantity = 1; } diff --git a/server/lib/billing/limitSet.ts b/server/lib/billing/limitSet.ts index e45ae637d..1273112ff 100644 --- a/server/lib/billing/limitSet.ts +++ b/server/lib/billing/limitSet.ts @@ -1,70 +1,70 @@ -import { FeatureId } from "./features"; +import { LimitId } from "./features"; export type LimitSet = Partial<{ - [key in FeatureId]: { + [key in LimitId]: { value: number | null; // null indicates no limit description?: string; }; }>; export const freeLimitSet: LimitSet = { - [FeatureId.SITES]: { value: 5, description: "Basic limit" }, - [FeatureId.USERS]: { value: 5, description: "Basic limit" }, - [FeatureId.DOMAINS]: { value: 5, description: "Basic limit" }, - [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" }, - [FeatureId.ORGINIZATIONS]: { value: 1, description: "Basic limit" }, + [LimitId.SITES]: { value: 5, description: "Basic limit" }, + [LimitId.USERS]: { value: 5, description: "Basic limit" }, + [LimitId.DOMAINS]: { value: 5, description: "Basic limit" }, + [LimitId.REMOTE_EXIT_NODES]: { value: 1, description: "Basic limit" }, + [LimitId.ORGINIZATIONS]: { value: 1, description: "Basic limit" } }; export const tier1LimitSet: LimitSet = { - [FeatureId.USERS]: { value: 7, description: "Home limit" }, - [FeatureId.SITES]: { value: 10, description: "Home limit" }, - [FeatureId.DOMAINS]: { value: 10, description: "Home limit" }, - [FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" }, - [FeatureId.ORGINIZATIONS]: { value: 1, description: "Home limit" }, + [LimitId.USERS]: { value: 7, description: "Home limit" }, + [LimitId.SITES]: { value: 10, description: "Home limit" }, + [LimitId.DOMAINS]: { value: 10, description: "Home limit" }, + [LimitId.REMOTE_EXIT_NODES]: { value: 1, description: "Home limit" }, + [LimitId.ORGINIZATIONS]: { value: 1, description: "Home limit" } }; export const tier2LimitSet: LimitSet = { - [FeatureId.USERS]: { + [LimitId.USERS]: { value: 50, description: "Team limit" }, - [FeatureId.SITES]: { + [LimitId.SITES]: { value: 50, description: "Team limit" }, - [FeatureId.DOMAINS]: { + [LimitId.DOMAINS]: { value: 50, description: "Team limit" }, - [FeatureId.REMOTE_EXIT_NODES]: { + [LimitId.REMOTE_EXIT_NODES]: { value: 3, description: "Team limit" }, - [FeatureId.ORGINIZATIONS]: { + [LimitId.ORGINIZATIONS]: { value: 1, description: "Team limit" } }; export const tier3LimitSet: LimitSet = { - [FeatureId.USERS]: { + [LimitId.USERS]: { value: 250, description: "Business limit" }, - [FeatureId.SITES]: { + [LimitId.SITES]: { value: 250, description: "Business limit" }, - [FeatureId.DOMAINS]: { + [LimitId.DOMAINS]: { value: 100, description: "Business limit" }, - [FeatureId.REMOTE_EXIT_NODES]: { + [LimitId.REMOTE_EXIT_NODES]: { value: 20, description: "Business limit" }, - [FeatureId.ORGINIZATIONS]: { + [LimitId.ORGINIZATIONS]: { value: 5, description: "Business limit" - }, + } }; diff --git a/server/lib/billing/limitsService.ts b/server/lib/billing/limitsService.ts index f364d6e00..e08b0fe14 100644 --- a/server/lib/billing/limitsService.ts +++ b/server/lib/billing/limitsService.ts @@ -1,7 +1,7 @@ import { db, limits } from "@server/db"; import { and, eq } from "drizzle-orm"; import { LimitSet } from "./limitSet"; -import { FeatureId } from "./features"; +import { LimitId } from "./features"; import logger from "@server/logger"; class LimitService { @@ -38,7 +38,7 @@ class LimitService { async getOrgLimit( orgId: string, - featureId: FeatureId + featureId: LimitId ): Promise { const limitId = `${orgId}-${featureId}`; const [limit] = await db diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts index dd32a09ad..a239cc719 100644 --- a/server/lib/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -9,7 +9,7 @@ import { Transaction, orgs } from "@server/db"; -import { FeatureId, getFeatureMeterId } from "./features"; +import { LimitId, getFeatureMeterId } from "./features"; import logger from "@server/logger"; import { build } from "@server/build"; import { regionalCache as cache } from "#dynamic/lib/cache"; @@ -37,7 +37,7 @@ export class UsageService { public async add( orgId: string, - featureId: FeatureId, + featureId: LimitId, value: number, transaction: any = null ): Promise { @@ -114,7 +114,7 @@ export class UsageService { private async internalAddUsage( orgId: string, // here the orgId is the billing org already resolved by getBillingOrg in updateCount - featureId: FeatureId, + featureId: LimitId, value: number, trx: Transaction ): Promise { @@ -163,7 +163,7 @@ export class UsageService { async updateCount( orgId: string, - featureId: FeatureId, + featureId: LimitId, value?: number, customerId?: string ): Promise { @@ -227,7 +227,7 @@ export class UsageService { private async getCustomerId( orgId: string, - featureId: FeatureId + featureId: LimitId ): Promise { const orgIdToUse = await this.getBillingOrg(orgId); @@ -269,7 +269,7 @@ export class UsageService { public async getUsage( orgId: string, - featureId: FeatureId, + featureId: LimitId, trx: Transaction | typeof db = db ): Promise { if (noop()) { @@ -376,7 +376,7 @@ export class UsageService { public async checkLimitSet( orgId: string, - featureId?: FeatureId, + featureId?: LimitId, usage?: Usage, trx: Transaction | typeof db = db ): Promise { @@ -424,7 +424,7 @@ export class UsageService { } else { currentUsage = await this.getUsage( orgIdToUse, - limit.featureId as FeatureId, + limit.featureId as LimitId, trx ); } diff --git a/server/lib/deleteOrg.ts b/server/lib/deleteOrg.ts index 065f216a1..0d954016f 100644 --- a/server/lib/deleteOrg.ts +++ b/server/lib/deleteOrg.ts @@ -24,7 +24,7 @@ import { deletePeer } from "@server/routers/gerbil/peers"; import { OlmErrorCodes } from "@server/routers/olm/error"; import { sendTerminateClient } from "@server/routers/client/terminate"; import { usageService } from "./billing/usageService"; -import { FeatureId } from "./billing"; +import { LimitId } from "./billing"; export type DeleteOrgByIdResult = { deletedNewtIds: string[]; @@ -140,7 +140,9 @@ export async function deleteOrgById( .select({ count: count() }) .from(orgDomains) .where(eq(orgDomains.domainId, domainId)); - logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`); + logger.info( + `Found ${orgCount.count} orgs using domain ${domainId}` + ); if (orgCount.count === 1) { domainIdsToDelete.push(domainId); } @@ -152,7 +154,7 @@ export async function deleteOrgById( .where(inArray(domains.domainId, domainIdsToDelete)); } - await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here + await usageService.add(orgId, LimitId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here await trx.delete(orgs).where(eq(orgs.orgId, orgId)); @@ -199,22 +201,22 @@ export async function deleteOrgById( if (org.billingOrgId) { usageService.updateCount( org.billingOrgId, - FeatureId.DOMAINS, + LimitId.DOMAINS, domainCount ?? 0 ); usageService.updateCount( org.billingOrgId, - FeatureId.SITES, + LimitId.SITES, siteCount ?? 0 ); usageService.updateCount( org.billingOrgId, - FeatureId.USERS, + LimitId.USERS, userCount ?? 0 ); usageService.updateCount( org.billingOrgId, - FeatureId.REMOTE_EXIT_NODES, + LimitId.REMOTE_EXIT_NODES, remoteExitNodeCount ?? 0 ); } diff --git a/server/lib/userOrg.ts b/server/lib/userOrg.ts index 809266b73..4bff40c13 100644 --- a/server/lib/userOrg.ts +++ b/server/lib/userOrg.ts @@ -14,7 +14,7 @@ import { } from "@server/db"; import { eq, and, inArray, ne, exists } from "drizzle-orm"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; export async function assignUserToOrg( org: Org, @@ -61,7 +61,7 @@ export async function assignUserToOrg( ); if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { - await usageService.add(org.orgId, FeatureId.USERS, 1, trx); + await usageService.add(org.orgId, LimitId.USERS, 1, trx); } } } @@ -157,7 +157,7 @@ export async function removeUserFromOrg( ); if (orgsInBillingDomainThatTheUserIsStillIn.length === 0) { - await usageService.add(org.orgId, FeatureId.USERS, -1, trx); + await usageService.add(org.orgId, LimitId.USERS, -1, trx); } } } diff --git a/server/private/routers/billing/changeTier.ts b/server/private/routers/billing/changeTier.ts index d82cbfeea..e532e88fd 100644 --- a/server/private/routers/billing/changeTier.ts +++ b/server/private/routers/billing/changeTier.ts @@ -25,7 +25,7 @@ import { getTier1FeaturePriceSet, getTier3FeaturePriceSet, getTier2FeaturePriceSet, - FeatureId, + LimitId, type FeaturePriceSet } from "@server/lib/billing"; import { getLineItems } from "@server/lib/billing/getLineItems"; @@ -214,7 +214,7 @@ export async function changeTier( } // Map to the corresponding feature in the new tier - const newPriceId = targetPriceSet[FeatureId.USERS]; + const newPriceId = targetPriceSet[LimitId.USERS]; if (newPriceId) { return { diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts index 718559654..7ef993a2b 100644 --- a/server/private/routers/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -24,7 +24,7 @@ import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { Limit, limits, Usage, usage } from "@server/db"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { GetOrgUsageResponse } from "@server/routers/billing/types"; const getOrgSchema = z.strictObject({ @@ -93,16 +93,16 @@ export async function getOrgUsage( // Get usage for org const usageData = []; - const sites = await usageService.getUsage(orgId, FeatureId.SITES); - const users = await usageService.getUsage(orgId, FeatureId.USERS); - const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS); + const sites = await usageService.getUsage(orgId, LimitId.SITES); + const users = await usageService.getUsage(orgId, LimitId.USERS); + const domains = await usageService.getUsage(orgId, LimitId.DOMAINS); const remoteExitNodes = await usageService.getUsage( orgId, - FeatureId.REMOTE_EXIT_NODES + LimitId.REMOTE_EXIT_NODES ); const organizations = await usageService.getUsage( orgId, - FeatureId.ORGINIZATIONS + LimitId.ORGINIZATIONS ); // const egressData = await usageService.getUsage( // orgId, diff --git a/server/private/routers/remoteExitNode/createRemoteExitNode.ts b/server/private/routers/remoteExitNode/createRemoteExitNode.ts index d7e889222..bb16f228d 100644 --- a/server/private/routers/remoteExitNode/createRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/createRemoteExitNode.ts @@ -35,7 +35,7 @@ import logger from "@server/logger"; import { and, eq, inArray, ne } from "drizzle-orm"; import { getNextAvailableSubnet } from "@server/lib/exitNodes"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; export const paramsSchema = z.object({ @@ -79,7 +79,10 @@ export async function createRemoteExitNode( const { remoteExitNodeId, secret } = parsedBody.data; - if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) { + if ( + req.user && + (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0) + ) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); @@ -87,13 +90,13 @@ export async function createRemoteExitNode( const usage = await usageService.getUsage( orgId, - FeatureId.REMOTE_EXIT_NODES + LimitId.REMOTE_EXIT_NODES ); if (usage) { const rejectRemoteExitNodes = await usageService.checkLimitSet( orgId, - FeatureId.REMOTE_EXIT_NODES, + LimitId.REMOTE_EXIT_NODES, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 @@ -264,7 +267,7 @@ export async function createRemoteExitNode( if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) { await usageService.add( orgId, - FeatureId.REMOTE_EXIT_NODES, + LimitId.REMOTE_EXIT_NODES, 1, trx ); diff --git a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts index e86476a5a..6fd871896 100644 --- a/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts +++ b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts @@ -22,7 +22,7 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; const paramsSchema = z.strictObject({ orgId: z.string().min(1), @@ -117,7 +117,7 @@ export async function deleteRemoteExitNode( if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) { await usageService.add( orgId, - FeatureId.REMOTE_EXIT_NODES, + LimitId.REMOTE_EXIT_NODES, -1, trx ); diff --git a/server/routers/auth/deleteMyAccount.ts b/server/routers/auth/deleteMyAccount.ts index d45486946..0a1f74f3e 100644 --- a/server/routers/auth/deleteMyAccount.ts +++ b/server/routers/auth/deleteMyAccount.ts @@ -20,7 +20,7 @@ import { getOrgTierData } from "#dynamic/lib/billing"; import { deleteOrgById, sendTerminationMessages } from "@server/lib/deleteOrg"; import { UserType } from "@server/types/UserTypes"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; const deleteMyAccountBody = z.strictObject({ password: z.string().optional(), @@ -220,7 +220,7 @@ export async function deleteMyAccount( await trx.delete(users).where(eq(users.userId, userId)); // loop through the other orgs and decrement the count for (const userOrg of otherOrgsTheUserWasIn) { - await usageService.add(userOrg.orgId, FeatureId.USERS, -1, trx); + await usageService.add(userOrg.orgId, LimitId.USERS, -1, trx); } }); diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index ceb61b25f..8370b5465 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -17,7 +17,7 @@ import { subdomainSchema } from "@server/lib/schemas"; import { generateId } from "@server/auth/sessions/app"; import { eq, and } from "drizzle-orm"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators"; import { build } from "@server/build"; import config from "@server/lib/config"; @@ -120,7 +120,7 @@ export async function createOrgDomain( } if (build == "saas") { - const usage = await usageService.getUsage(orgId, FeatureId.DOMAINS); + const usage = await usageService.getUsage(orgId, LimitId.DOMAINS); if (!usage) { return next( createHttpError( @@ -132,7 +132,7 @@ export async function createOrgDomain( const rejectDomains = await usageService.checkLimitSet( orgId, - FeatureId.DOMAINS, + LimitId.DOMAINS, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 @@ -346,7 +346,7 @@ export async function createOrgDomain( await trx.insert(dnsRecords).values(recordsToInsert); } - await usageService.add(orgId, FeatureId.DOMAINS, 1, trx); + await usageService.add(orgId, LimitId.DOMAINS, 1, trx); }); if (!returned) { diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts index 4c347668e..3f443eac5 100644 --- a/server/routers/domain/deleteOrgDomain.ts +++ b/server/routers/domain/deleteOrgDomain.ts @@ -8,7 +8,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { and, eq } from "drizzle-orm"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; const paramsSchema = z.strictObject({ domainId: z.string(), @@ -77,7 +77,7 @@ export async function deleteAccountDomain( await trx.delete(domains).where(eq(domains.domainId, domainId)); - await usageService.add(orgId, FeatureId.DOMAINS, -1, trx); + await usageService.add(orgId, LimitId.DOMAINS, -1, trx); }); return response(res, { diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index eacf3dad4..aacf7f843 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -6,7 +6,7 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing/features"; +import { LimitId } from "@server/lib/billing/features"; import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; @@ -171,8 +171,9 @@ export async function flushSiteBandwidthToDb(): Promise { } // PostgreSQL: batch UPDATE … FROM (VALUES …) - single round-trip per chunk. - const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) => - sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)` + const valuesList = chunk.map( + ([publicKey, { bytesIn, bytesOut }]) => + sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)` ); const valuesClause = sql.join(valuesList, sql`, `); return dbQueryRows<{ orgId: string; pubKey: string }>(sql` @@ -228,7 +229,7 @@ export async function flushSiteBandwidthToDb(): Promise { const totalBandwidth = orgUsageMap.get(orgId)!; const bandwidthUsage = await usageService.add( orgId, - FeatureId.EGRESS_DATA_MB, + LimitId.EGRESS_DATA_MB, totalBandwidth ); if (bandwidthUsage) { @@ -236,7 +237,7 @@ export async function flushSiteBandwidthToDb(): Promise { usageService .checkLimitSet( orgId, - FeatureId.EGRESS_DATA_MB, + LimitId.EGRESS_DATA_MB, bandwidthUsage ) .catch((error: any) => { @@ -247,10 +248,7 @@ export async function flushSiteBandwidthToDb(): Promise { }); } } catch (error) { - logger.error( - `Error processing usage for org ${orgId}:`, - error - ); + logger.error(`Error processing usage for org ${orgId}:`, error); // Continue with other orgs. } } diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index f6cb656b3..57b16964d 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -31,7 +31,7 @@ import { } from "@server/auth/sessions/app"; import { decrypt } from "@server/lib/crypto"; import { UserType } from "@server/types/UserTypes"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { usageService } from "@server/lib/billing/usageService"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; @@ -645,7 +645,7 @@ export async function validateOidcCallback( for (const orgCount of orgUserCounts) { await usageService.updateCount( orgCount.orgId, - FeatureId.USERS, + LimitId.USERS, orgCount.userCount ); } diff --git a/server/routers/newt/registerNewt.ts b/server/routers/newt/registerNewt.ts index 3adcfb467..1fbfab191 100644 --- a/server/routers/newt/registerNewt.ts +++ b/server/routers/newt/registerNewt.ts @@ -25,7 +25,7 @@ import { getUniqueSiteName } from "@server/db/names"; import moment from "moment"; import { build } from "@server/build"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { INSPECT_MAX_BYTES } from "buffer"; import { getNextAvailableClientSubnet } from "@server/lib/ip"; @@ -169,7 +169,7 @@ export async function registerNewt( // SaaS billing check if (build == "saas") { - const usage = await usageService.getUsage(orgId, FeatureId.SITES); + const usage = await usageService.getUsage(orgId, LimitId.SITES); if (!usage) { return next( createHttpError( @@ -180,7 +180,7 @@ export async function registerNewt( } const rejectSites = await usageService.checkLimitSet( orgId, - FeatureId.SITES, + LimitId.SITES, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 @@ -274,7 +274,7 @@ export async function registerNewt( ) ); - await usageService.add(orgId, FeatureId.SITES, 1, trx); + await usageService.add(orgId, LimitId.SITES, 1, trx); }); } finally { await releaseSubnetLock(); diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 35466ebc0..c625e42af 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -27,7 +27,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import { isValidCIDR } from "@server/lib/validators"; import { createCustomer } from "#dynamic/lib/billing"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId, limitsService, freeLimitSet } from "@server/lib/billing"; +import { LimitId, limitsService, freeLimitSet } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { doCidrsOverlap } from "@server/lib/ip"; @@ -202,7 +202,7 @@ export async function createOrg( if (build == "saas" && billingOrgIdForNewOrg) { const usage = await usageService.getUsage( billingOrgIdForNewOrg, - FeatureId.ORGINIZATIONS + LimitId.ORGINIZATIONS ); if (!usage) { return next( @@ -214,7 +214,7 @@ export async function createOrg( } const rejectOrgs = await usageService.checkLimitSet( billingOrgIdForNewOrg, - FeatureId.ORGINIZATIONS, + LimitId.ORGINIZATIONS, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 @@ -421,7 +421,7 @@ export async function createOrg( if (customerId) { await usageService.updateCount( orgId, - FeatureId.USERS, + LimitId.USERS, 1, customerId ); // Only 1 because we are creating the org @@ -431,7 +431,7 @@ export async function createOrg( if (numOrgs) { usageService.updateCount( billingOrgIdForNewOrg || orgId, - FeatureId.ORGINIZATIONS, + LimitId.ORGINIZATIONS, numOrgs ); } diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index cc67f7b27..bddf5b251 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -19,7 +19,7 @@ import { getNextAvailableClientSubnet, isIpInCidr } from "@server/lib/ip"; import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { generateId } from "@server/auth/sessions/app"; const createSiteParamsSchema = z.strictObject({ @@ -160,7 +160,7 @@ export async function createSite( } if (build == "saas") { - const usage = await usageService.getUsage(orgId, FeatureId.SITES); + const usage = await usageService.getUsage(orgId, LimitId.SITES); if (!usage) { return next( createHttpError( @@ -172,7 +172,7 @@ export async function createSite( const rejectSites = await usageService.checkLimitSet( orgId, - FeatureId.SITES, + LimitId.SITES, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 @@ -519,7 +519,7 @@ export async function createSite( }); } - await usageService.add(orgId, FeatureId.SITES, 1, trx); + await usageService.add(orgId, LimitId.SITES, 1, trx); }); } finally { await releaseSubnetLock?.(); diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts index 300c570d8..602032ffb 100644 --- a/server/routers/site/deleteSite.ts +++ b/server/routers/site/deleteSite.ts @@ -13,7 +13,7 @@ import { sendToClient } from "#dynamic/routers/ws"; import { OpenAPITags, registry } from "@server/openApi"; import { cleanupSiteAssociations } from "@server/lib/rebuildClientAssociations"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import { deleteAssociatedResourcesForSite, @@ -177,7 +177,7 @@ export async function deleteSite( } await trx.delete(sites).where(eq(sites.siteId, siteId)); - await usageService.add(site.orgId, FeatureId.SITES, -1, trx); + await usageService.add(site.orgId, LimitId.SITES, -1, trx); }); if (deleteResources) { diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts index 37ac69f59..c912fea0e 100644 --- a/server/routers/user/acceptInvite.ts +++ b/server/routers/user/acceptInvite.ts @@ -17,7 +17,7 @@ import { fromError } from "zod-validation-error"; import { checkValidInvite } from "@server/auth/checkValidInvite"; import { verifySession } from "@server/auth/sessions/verifySession"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { build } from "@server/build"; import { assignUserToOrg } from "@server/lib/userOrg"; @@ -104,7 +104,7 @@ export async function acceptInvite( if (build == "saas") { const usage = await usageService.getUsage( existingInvite.orgId, - FeatureId.USERS + LimitId.USERS ); if (!usage) { return next( @@ -117,7 +117,7 @@ export async function acceptInvite( const rejectUsers = await usageService.checkLimitSet( existingInvite.orgId, - FeatureId.USERS, + LimitId.USERS, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index b20881198..d80d85999 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -12,7 +12,7 @@ import { and, eq, inArray } from "drizzle-orm"; import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db"; import { generateId } from "@server/auth/sessions/app"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { isSubscribed } from "#dynamic/lib/isSubscribed"; @@ -123,7 +123,7 @@ export async function createOrgUser( } = parsedBody.data; if (build == "saas") { - const usage = await usageService.getUsage(orgId, FeatureId.USERS); + const usage = await usageService.getUsage(orgId, LimitId.USERS); if (!usage) { return next( createHttpError( @@ -135,7 +135,7 @@ export async function createOrgUser( const rejectUsers = await usageService.checkLimitSet( orgId, - FeatureId.USERS, + LimitId.USERS, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index c4feb820f..7445ee910 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -25,7 +25,7 @@ import SendInviteLink from "@server/emails/templates/SendInviteLink"; import { OpenAPITags, registry } from "@server/openApi"; import { UserType } from "@server/types/UserTypes"; import { usageService } from "@server/lib/billing/usageService"; -import { FeatureId } from "@server/lib/billing"; +import { LimitId } from "@server/lib/billing"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { build } from "@server/build"; import cache from "#dynamic/lib/cache"; @@ -73,7 +73,6 @@ const InviteUserResponseDataSchema = z.object({ expiresAt: z.number() }); - registry.registerPath({ method: "post", path: "/org/{orgId}/create-invite", @@ -94,7 +93,9 @@ registry.registerPath({ description: "Successful response", content: { "application/json": { - schema: createApiResponseSchema(InviteUserResponseDataSchema) + schema: createApiResponseSchema( + InviteUserResponseDataSchema + ) } } } @@ -181,7 +182,7 @@ export async function inviteUser( } if (build == "saas") { - const usage = await usageService.getUsage(orgId, FeatureId.USERS); + const usage = await usageService.getUsage(orgId, LimitId.USERS); if (!usage) { return next( createHttpError( @@ -192,7 +193,7 @@ export async function inviteUser( } const rejectUsers = await usageService.checkLimitSet( orgId, - FeatureId.USERS, + LimitId.USERS, { ...usage, instantaneousValue: (usage.instantaneousValue || 0) + 1 diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index e4abdb561..34b0ee3cc 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -58,7 +58,7 @@ import { tier2LimitSet, tier3LimitSet } from "@server/lib/billing/limitSet"; -import { FeatureId } from "@server/lib/billing/features"; +import { LimitId } from "@server/lib/billing/features"; import TrialBillingBanner from "@app/components/TrialBillingBanner"; // Plan tier definitions matching the mockup @@ -161,32 +161,32 @@ const tierLimits: Record< } > = { basic: { - users: freeLimitSet[FeatureId.USERS]?.value ?? 0, - sites: freeLimitSet[FeatureId.SITES]?.value ?? 0, - domains: freeLimitSet[FeatureId.DOMAINS]?.value ?? 0, - remoteNodes: freeLimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: freeLimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 + users: freeLimitSet[LimitId.USERS]?.value ?? 0, + sites: freeLimitSet[LimitId.SITES]?.value ?? 0, + domains: freeLimitSet[LimitId.DOMAINS]?.value ?? 0, + remoteNodes: freeLimitSet[LimitId.REMOTE_EXIT_NODES]?.value ?? 0, + organizations: freeLimitSet[LimitId.ORGINIZATIONS]?.value ?? 0 }, tier1: { - users: tier1LimitSet[FeatureId.USERS]?.value ?? 0, - sites: tier1LimitSet[FeatureId.SITES]?.value ?? 0, - domains: tier1LimitSet[FeatureId.DOMAINS]?.value ?? 0, - remoteNodes: tier1LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: tier1LimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 + 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.ORGINIZATIONS]?.value ?? 0 }, tier2: { - users: tier2LimitSet[FeatureId.USERS]?.value ?? 0, - sites: tier2LimitSet[FeatureId.SITES]?.value ?? 0, - domains: tier2LimitSet[FeatureId.DOMAINS]?.value ?? 0, - remoteNodes: tier2LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: tier2LimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 + 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.ORGINIZATIONS]?.value ?? 0 }, tier3: { - users: tier3LimitSet[FeatureId.USERS]?.value ?? 0, - sites: tier3LimitSet[FeatureId.SITES]?.value ?? 0, - domains: tier3LimitSet[FeatureId.DOMAINS]?.value ?? 0, - remoteNodes: tier3LimitSet[FeatureId.REMOTE_EXIT_NODES]?.value ?? 0, - organizations: tier3LimitSet[FeatureId.ORGINIZATIONS]?.value ?? 0 + 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.ORGINIZATIONS]?.value ?? 0 }, enterprise: { users: 0, // Custom for enterprise