mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-06 18:10:32 +00:00
Compare commits
1 Commits
dependabot
...
new-pricin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f899326189 |
4
.github/workflows/cicd.yml
vendored
4
.github/workflows/cicd.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -576,7 +576,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
2
.github/workflows/restart-runners.yml
vendored
2
.github/workflows/restart-runners.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
6
.github/workflows/saas.yml
vendored
6
.github/workflows/saas.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
|
||||
"siteSettingDescription": "Standorteinstellungen konfigurieren",
|
||||
"siteSetting": "{siteName} Einstellungen",
|
||||
"siteNewtTunnel": "Newt Standort (empfohlen)",
|
||||
"siteNewtTunnel": "Neuer Standort (empfohlen)",
|
||||
"siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.",
|
||||
"siteWg": "Einfacher WireGuard Tunnel",
|
||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
||||
@@ -107,7 +107,7 @@
|
||||
"siteSeeAll": "Alle Standorte anzeigen",
|
||||
"siteTunnelDescription": "Legen Sie fest, wie Sie sich mit dem Standort verbinden möchten",
|
||||
"siteNewtCredentials": "Zugangsdaten",
|
||||
"siteNewtCredentialsDescription": "So wird sich der Standort mit dem Server authentifizieren",
|
||||
"siteNewtCredentialsDescription": "So wird sich die Seite mit dem Server authentifizieren",
|
||||
"remoteNodeCredentialsDescription": "So wird sich der entfernte Node mit dem Server authentifizieren",
|
||||
"siteCredentialsSave": "Anmeldedaten speichern",
|
||||
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
|
||||
@@ -2503,7 +2503,7 @@
|
||||
"deviceModel": "Gerätemodell",
|
||||
"serialNumber": "Seriennummer",
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Zuerst gesehen",
|
||||
"firstSeen": "Erster Blick",
|
||||
"lastSeen": "Zuletzt gesehen",
|
||||
"biometricsEnabled": "Biometrie aktiviert",
|
||||
"diskEncrypted": "Festplatte verschlüsselt",
|
||||
|
||||
@@ -1404,7 +1404,7 @@
|
||||
"billingUsageLimitsOverview": "Usage Limits Overview",
|
||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||
"billingDataUsage": "Data Usage",
|
||||
"billingOnlineTime": "Site Online Time",
|
||||
"billingSites": "Sites",
|
||||
"billingUsers": "Active Users",
|
||||
"billingDomains": "Active Domains",
|
||||
"billingRemoteExitNodes": "Active Self-hosted Nodes",
|
||||
@@ -1432,10 +1432,10 @@
|
||||
"billingFailedToGetPortalUrl": "Failed to get portal URL",
|
||||
"billingPortalError": "Portal Error",
|
||||
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
||||
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
|
||||
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
|
||||
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
|
||||
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
|
||||
"billingSInfo": "How many sites you can use",
|
||||
"billingUsersInfo": "How many users you can use",
|
||||
"billingDomainInfo": "How many domains you can use",
|
||||
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
|
||||
"billingLicenseKeys": "License Keys",
|
||||
"billingLicenseKeysDescription": "Manage your license key subscriptions",
|
||||
"billingLicenseSubscription": "License Subscription",
|
||||
@@ -1444,7 +1444,6 @@
|
||||
"billingQuantity": "Quantity",
|
||||
"billingTotal": "total",
|
||||
"billingModifyLicenses": "Modify License Subscription",
|
||||
"billingPricingCalculatorLink": "View Pricing Calculator",
|
||||
"domainNotFound": "Domain Not Found",
|
||||
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
||||
"failed": "Failed",
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
import Stripe from "stripe";
|
||||
|
||||
export enum FeatureId {
|
||||
SITE_UPTIME = "siteUptime",
|
||||
USERS = "users",
|
||||
SITES = "sites",
|
||||
EGRESS_DATA_MB = "egressDataMb",
|
||||
DOMAINS = "domains",
|
||||
REMOTE_EXIT_NODES = "remoteExitNodes"
|
||||
}
|
||||
|
||||
export const FeatureMeterIds: Record<FeatureId, string> = {
|
||||
[FeatureId.SITE_UPTIME]: "mtr_61Srrej5wUJuiTWgo41D3Ee2Ir7WmDLU",
|
||||
[FeatureId.USERS]: "mtr_61SrreISyIWpwUNGR41D3Ee2Ir7WmQro",
|
||||
[FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW",
|
||||
[FeatureId.DOMAINS]: "mtr_61Ss9nIKDNMw0LDRU41D3Ee2Ir7WmRPU",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "mtr_61T86UXnfxTVXy9sD41D3Ee2Ir7WmFTE"
|
||||
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = {
|
||||
[FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
||||
};
|
||||
|
||||
export const FeatureMeterIdsSandbox: Record<FeatureId, string> = {
|
||||
[FeatureId.SITE_UPTIME]: "mtr_test_61Snh3cees4w60gv841DCpkOb237BDEu",
|
||||
[FeatureId.USERS]: "mtr_test_61Sn5fLtq1gSfRkyA41DCpkOb237B6au",
|
||||
[FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ",
|
||||
[FeatureId.DOMAINS]: "mtr_test_61SsA8qrdAlgPpFRQ41DCpkOb237BGts",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "mtr_test_61T86Vqmwa3D9ra3341DCpkOb237B94K"
|
||||
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
|
||||
[FeatureId.EGRESS_DATA_MB]: "mtr_test_61Snh2a2m6qome5Kv41DCpkOb237B3dQ"
|
||||
};
|
||||
|
||||
export function getFeatureMeterId(featureId: FeatureId): string {
|
||||
export function getFeatureMeterId(featureId: FeatureId): string | undefined {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
@@ -43,38 +35,43 @@ export function getFeatureIdByMetricId(
|
||||
)?.[0];
|
||||
}
|
||||
|
||||
export type FeaturePriceSet = {
|
||||
[key in Exclude<FeatureId, FeatureId.DOMAINS>]: string;
|
||||
} & {
|
||||
[FeatureId.DOMAINS]?: string; // Optional since domains are not billed
|
||||
export type FeaturePriceSet = Partial<Record<FeatureId, string>>;
|
||||
|
||||
export const starterFeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea"
|
||||
};
|
||||
|
||||
export const standardFeaturePriceSet: FeaturePriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[FeatureId.SITE_UPTIME]: "price_1RrQc4D3Ee2Ir7WmaJGZ3MtF",
|
||||
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea",
|
||||
[FeatureId.EGRESS_DATA_MB]: "price_1RrQXFD3Ee2Ir7WmvGDlgxQk",
|
||||
// [FeatureId.DOMAINS]: "price_1Rz3tMD3Ee2Ir7Wm5qLeASzC",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1S46weD3Ee2Ir7Wm94KEHI4h"
|
||||
export const starterFeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF"
|
||||
};
|
||||
|
||||
export const standardFeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
// Free tier matches the freeLimitSet
|
||||
[FeatureId.SITE_UPTIME]: "price_1RefFBDCpkOb237BPrKZ8IEU",
|
||||
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF",
|
||||
[FeatureId.EGRESS_DATA_MB]: "price_1Rfp9LDCpkOb237BwuN5Oiu0",
|
||||
// [FeatureId.DOMAINS]: "price_1Ryi88DCpkOb237B2D6DM80b",
|
||||
[FeatureId.REMOTE_EXIT_NODES]: "price_1RyiZvDCpkOb237BXpmoIYJL"
|
||||
};
|
||||
|
||||
export function getStandardFeaturePriceSet(): FeaturePriceSet {
|
||||
export function getStarterFeaturePriceSet(): FeaturePriceSet {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return standardFeaturePriceSet;
|
||||
return starterFeaturePriceSet;
|
||||
} else {
|
||||
return standardFeaturePriceSetSandbox;
|
||||
return starterFeaturePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
export const scaleFeaturePriceSet: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1RrQeJD3Ee2Ir7WmgveP3xea"
|
||||
};
|
||||
|
||||
export const scaleFeaturePriceSetSandbox: FeaturePriceSet = {
|
||||
[FeatureId.USERS]: "price_1ReNa4DCpkOb237Bc67G5muF"
|
||||
};
|
||||
|
||||
export function getScaleFeaturePriceSet(): FeaturePriceSet {
|
||||
if (
|
||||
process.env.ENVIRONMENT == "prod" &&
|
||||
process.env.SANDBOX_MODE !== "true"
|
||||
) {
|
||||
return scaleFeaturePriceSet;
|
||||
} else {
|
||||
return scaleFeaturePriceSetSandbox;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export type LimitSet = {
|
||||
};
|
||||
|
||||
export const sandboxLimitSet: LimitSet = {
|
||||
[FeatureId.SITE_UPTIME]: { value: 2880, description: "Sandbox limit" }, // 1 site up for 2 days
|
||||
[FeatureId.SITES]: { value: 1, description: "Sandbox limit" }, // 1 site up for 2 days
|
||||
[FeatureId.USERS]: { value: 1, description: "Sandbox limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: { value: 1000, description: "Sandbox limit" }, // 1 GB
|
||||
[FeatureId.DOMAINS]: { value: 0, description: "Sandbox limit" },
|
||||
@@ -16,7 +16,7 @@ export const sandboxLimitSet: LimitSet = {
|
||||
};
|
||||
|
||||
export const freeLimitSet: LimitSet = {
|
||||
[FeatureId.SITE_UPTIME]: { value: 46080, description: "Free tier limit" }, // 1 site up for 32 days
|
||||
[FeatureId.SITES]: { value: 3, description: "Free tier limit" }, // 1 site up for 32 days
|
||||
[FeatureId.USERS]: { value: 3, description: "Free tier limit" },
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 25000,
|
||||
@@ -26,9 +26,32 @@ export const freeLimitSet: LimitSet = {
|
||||
[FeatureId.REMOTE_EXIT_NODES]: { value: 1, description: "Free tier limit" }
|
||||
};
|
||||
|
||||
export const subscribedLimitSet: LimitSet = {
|
||||
[FeatureId.SITE_UPTIME]: {
|
||||
value: 2232000,
|
||||
export const starterLimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: {
|
||||
value: 10,
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 50 sites up for 31 days
|
||||
[FeatureId.USERS]: {
|
||||
value: 150,
|
||||
description: "Contact us to increase soft limit."
|
||||
},
|
||||
[FeatureId.EGRESS_DATA_MB]: {
|
||||
value: 12000000,
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 12000 GB
|
||||
[FeatureId.DOMAINS]: {
|
||||
value: 250,
|
||||
description: "Contact us to increase soft limit."
|
||||
},
|
||||
[FeatureId.REMOTE_EXIT_NODES]: {
|
||||
value: 5,
|
||||
description: "Contact us to increase soft limit."
|
||||
}
|
||||
};
|
||||
|
||||
export const scaleLimitSet: LimitSet = {
|
||||
[FeatureId.SITES]: {
|
||||
value: 10,
|
||||
description: "Contact us to increase soft limit."
|
||||
}, // 50 sites up for 31 days
|
||||
[FeatureId.USERS]: {
|
||||
|
||||
@@ -78,9 +78,9 @@ export async function getOrgUsage(
|
||||
// Get usage for org
|
||||
const usageData = [];
|
||||
|
||||
const siteUptime = await usageService.getUsage(
|
||||
const sites = await usageService.getUsage(
|
||||
orgId,
|
||||
FeatureId.SITE_UPTIME
|
||||
FeatureId.SITES
|
||||
);
|
||||
const users = await usageService.getUsageDaily(orgId, FeatureId.USERS);
|
||||
const domains = await usageService.getUsageDaily(
|
||||
@@ -96,8 +96,8 @@ export async function getOrgUsage(
|
||||
FeatureId.EGRESS_DATA_MB
|
||||
);
|
||||
|
||||
if (siteUptime) {
|
||||
usageData.push(siteUptime);
|
||||
if (sites) {
|
||||
usageData.push(sites);
|
||||
}
|
||||
if (users) {
|
||||
usageData.push(users);
|
||||
|
||||
@@ -114,7 +114,6 @@ export async function updateSiteBandwidth(
|
||||
|
||||
// Aggregate usage data by organization (collected outside transaction)
|
||||
const orgUsageMap = new Map<string, number>();
|
||||
const orgUptimeMap = new Map<string, number>();
|
||||
|
||||
if (activePeers.length > 0) {
|
||||
// Remove any active peers from offline tracking since they're sending data
|
||||
@@ -166,14 +165,6 @@ export async function updateSiteBandwidth(
|
||||
updatedSite.orgId,
|
||||
currentOrgUsage + totalBandwidth
|
||||
);
|
||||
|
||||
// Add 10 seconds of uptime for each active site
|
||||
const currentOrgUptime =
|
||||
orgUptimeMap.get(updatedSite.orgId) || 0;
|
||||
orgUptimeMap.set(
|
||||
updatedSite.orgId,
|
||||
currentOrgUptime + 10 / 60
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
@@ -187,10 +178,10 @@ export async function updateSiteBandwidth(
|
||||
|
||||
// Process usage updates outside of site update transactions
|
||||
// This separates the concerns and reduces lock contention
|
||||
if (calcUsageAndLimits && (orgUsageMap.size > 0 || orgUptimeMap.size > 0)) {
|
||||
if (calcUsageAndLimits && (orgUsageMap.size > 0)) {
|
||||
// Sort org IDs to ensure consistent lock ordering
|
||||
const allOrgIds = [
|
||||
...new Set([...orgUsageMap.keys(), ...orgUptimeMap.keys()])
|
||||
...new Set([...orgUsageMap.keys()])
|
||||
].sort();
|
||||
|
||||
for (const orgId of allOrgIds) {
|
||||
@@ -220,32 +211,6 @@ export async function updateSiteBandwidth(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process uptime usage for this org
|
||||
const totalUptime = orgUptimeMap.get(orgId);
|
||||
if (totalUptime) {
|
||||
const uptimeUsage = await usageService.add(
|
||||
orgId,
|
||||
FeatureId.SITE_UPTIME,
|
||||
totalUptime
|
||||
);
|
||||
if (uptimeUsage) {
|
||||
// Fire and forget - don't block on limit checking
|
||||
usageService
|
||||
.checkLimitSet(
|
||||
orgId,
|
||||
true,
|
||||
FeatureId.SITE_UPTIME,
|
||||
uptimeUsage
|
||||
)
|
||||
.catch((error: any) => {
|
||||
logger.error(
|
||||
`Error checking uptime limits for org ${orgId}:`,
|
||||
error
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Error processing usage for org ${orgId}:`, error);
|
||||
// Continue with other orgs
|
||||
|
||||
@@ -96,10 +96,10 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
fetchContainers(newt.newtId);
|
||||
}
|
||||
|
||||
const rejectSiteUptime = await usageService.checkLimitSet(
|
||||
const rejectSites = await usageService.checkLimitSet(
|
||||
oldSite.orgId,
|
||||
false,
|
||||
FeatureId.SITE_UPTIME
|
||||
FeatureId.SITES
|
||||
);
|
||||
const rejectEgressDataMb = await usageService.checkLimitSet(
|
||||
oldSite.orgId,
|
||||
@@ -111,8 +111,8 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
// const rejectUsers = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.USERS);
|
||||
// const rejectDomains = await usageService.checkLimitSet(oldSite.orgId, false, FeatureId.DOMAINS);
|
||||
|
||||
// if (rejectEgressDataMb || rejectSiteUptime || rejectUsers || rejectDomains) {
|
||||
if (rejectEgressDataMb || rejectSiteUptime) {
|
||||
// if (rejectEgressDataMb || rejectSites || rejectUsers || rejectDomains) {
|
||||
if (rejectEgressDataMb || rejectSites) {
|
||||
logger.info(
|
||||
`Usage limits exceeded for org ${oldSite.orgId}. Rejecting newt registration.`
|
||||
);
|
||||
|
||||
@@ -17,6 +17,9 @@ import { hashPassword } from "@server/auth/password";
|
||||
import { isValidIP } from "@server/lib/validators";
|
||||
import { 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";
|
||||
|
||||
const createSiteParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -125,6 +128,35 @@ export async function createSite(
|
||||
);
|
||||
}
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(orgId, FeatureId.SITES);
|
||||
if (!usage) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"No usage data found for this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
const rejectSites = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
false,
|
||||
FeatureId.SITES,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
} // We need to add one to know if we are violating the limit
|
||||
);
|
||||
if (rejectSites) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Sites limit exceeded. Please upgrade your plan."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let updatedAddress = null;
|
||||
if (address) {
|
||||
if (!org.subnet) {
|
||||
@@ -255,22 +287,10 @@ export async function createSite(
|
||||
|
||||
const niceId = await getUniqueSiteName(orgId);
|
||||
|
||||
let newSite: Site;
|
||||
|
||||
let newSite: Site | undefined;
|
||||
let numSites: Site[] | undefined;
|
||||
await db.transaction(async (trx) => {
|
||||
if (type == "newt") {
|
||||
[newSite] = await trx
|
||||
.insert(sites)
|
||||
.values({
|
||||
orgId,
|
||||
name,
|
||||
niceId,
|
||||
address: updatedAddress || null,
|
||||
type,
|
||||
dockerSocketEnabled: true
|
||||
})
|
||||
.returning();
|
||||
} else if (type == "wireguard") {
|
||||
if (type == "wireguard" || type == "newt") {
|
||||
// we are creating a site with an exit node (tunneled)
|
||||
if (!subnet) {
|
||||
return next(
|
||||
@@ -322,9 +342,11 @@ export async function createSite(
|
||||
exitNodeId,
|
||||
name,
|
||||
niceId,
|
||||
address: updatedAddress || null,
|
||||
subnet,
|
||||
type,
|
||||
pubKey: pubKey || null
|
||||
dockerSocketEnabled: type == "newt",
|
||||
...(pubKey && type == "wireguard" && { pubKey })
|
||||
})
|
||||
.returning();
|
||||
} else if (type == "local") {
|
||||
@@ -411,13 +433,35 @@ export async function createSite(
|
||||
});
|
||||
}
|
||||
|
||||
return response<CreateSiteResponse>(res, {
|
||||
data: newSite,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Site created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
numSites = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, orgId));
|
||||
});
|
||||
|
||||
if (numSites) {
|
||||
await usageService.updateDaily(
|
||||
orgId,
|
||||
FeatureId.SITES,
|
||||
numSites.length
|
||||
);
|
||||
}
|
||||
|
||||
if (!newSite) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create site"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response<CreateSiteResponse>(res, {
|
||||
data: newSite,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Site created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, siteResources } from "@server/db";
|
||||
import { db, Site, siteResources } from "@server/db";
|
||||
import { newts, newtSessions, sites } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
@@ -12,6 +12,8 @@ import { fromError } from "zod-validation-error";
|
||||
import { sendToClient } from "#dynamic/routers/ws";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { FeatureId } from "@server/lib/billing";
|
||||
|
||||
const deleteSiteSchema = z.strictObject({
|
||||
siteId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -62,6 +64,7 @@ export async function deleteSite(
|
||||
}
|
||||
|
||||
let deletedNewtId: string | null = null;
|
||||
let numSites: Site[] | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (site.type == "wireguard") {
|
||||
@@ -99,8 +102,20 @@ export async function deleteSite(
|
||||
}
|
||||
|
||||
await trx.delete(sites).where(eq(sites.siteId, siteId));
|
||||
|
||||
numSites = await trx
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.orgId, site.orgId));
|
||||
});
|
||||
|
||||
if (numSites) {
|
||||
await usageService.updateDaily(
|
||||
site.orgId,
|
||||
FeatureId.SITES,
|
||||
numSites.length
|
||||
);
|
||||
}
|
||||
// Send termination message outside of transaction to prevent blocking
|
||||
if (deletedNewtId) {
|
||||
const payload = {
|
||||
|
||||
@@ -207,7 +207,7 @@ export default function GeneralPage() {
|
||||
};
|
||||
|
||||
// Usage IDs
|
||||
const SITE_UPTIME = "siteUptime";
|
||||
const SITES = "sites";
|
||||
const USERS = "users";
|
||||
const EGRESS_DATA_MB = "egressDataMb";
|
||||
const DOMAINS = "domains";
|
||||
@@ -362,12 +362,11 @@ export default function GeneralPage() {
|
||||
getLimitUsage: (v: any) => v.latestValue
|
||||
},
|
||||
{
|
||||
id: SITE_UPTIME,
|
||||
label: t("billingOnlineTime"),
|
||||
id: SITES,
|
||||
label: t("billingSites"),
|
||||
icon: <Clock className="h-4 w-4 text-green-500" />,
|
||||
unit: "min",
|
||||
info: t("billingOnlineTimeInfo"),
|
||||
note: "Not counted on self-hosted nodes",
|
||||
unit: "",
|
||||
info: t("billingSitesInfo"),
|
||||
getDisplay: (v: any) => v.latestValue,
|
||||
getLimitDisplay: (v: any) => v.value,
|
||||
getUsage: (v: any) => v.latestValue,
|
||||
|
||||
Reference in New Issue
Block a user