mirror of
https://github.com/fosrl/pangolin.git
synced 2026-06-30 11:09:50 +00:00
Compare commits
19 Commits
dependabot
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5652cdb8a | ||
|
|
7c2ea153c5 | ||
|
|
ccabddc225 | ||
|
|
42d98fa83b | ||
|
|
2f2b7f43c1 | ||
|
|
528bbeca26 | ||
|
|
d60c15b0ae | ||
|
|
ff89a64453 | ||
|
|
4718c489d3 | ||
|
|
d5d99a4804 | ||
|
|
faee9e6330 | ||
|
|
31725eb3cc | ||
|
|
04d4e298e8 | ||
|
|
7506c0420d | ||
|
|
5572822c4a | ||
|
|
ea3f1c341b | ||
|
|
35dffe71cb | ||
|
|
5428bf4ed0 | ||
|
|
9a89579e08 |
@@ -1,28 +1,33 @@
|
||||
export enum FeatureId {
|
||||
export enum LimitId {
|
||||
USERS = "users",
|
||||
SITES = "sites",
|
||||
EGRESS_DATA_MB = "egressDataMb",
|
||||
DOMAINS = "domains",
|
||||
REMOTE_EXIT_NODES = "remoteExitNodes",
|
||||
ORGINIZATIONS = "organizations",
|
||||
ORGANIZATIONS = "organizations",
|
||||
PUBLIC_RESOURCES = "publicResources",
|
||||
PRIVATE_RESOURCES = "privateResources",
|
||||
MACHINE_CLIENTS = "machineClients",
|
||||
TIER1 = "tier1"
|
||||
}
|
||||
|
||||
export async function getFeatureDisplayName(featureId: FeatureId): Promise<string> {
|
||||
export async function getFeatureDisplayName(
|
||||
featureId: LimitId
|
||||
): Promise<string> {
|
||||
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.ORGANIZATIONS:
|
||||
return "Organizations";
|
||||
case FeatureId.TIER1:
|
||||
case LimitId.TIER1:
|
||||
return "Home Lab";
|
||||
default:
|
||||
return featureId;
|
||||
@@ -30,15 +35,16 @@ export async function getFeatureDisplayName(featureId: FeatureId): Promise<strin
|
||||
}
|
||||
|
||||
// this is from the old system
|
||||
export const FeatureMeterIds: Partial<Record<FeatureId, string>> = { // right now we are not charging for any data
|
||||
export const FeatureMeterIds: Partial<Record<LimitId, string>> = {
|
||||
// right now we are not charging for any data
|
||||
// [FeatureId.EGRESS_DATA_MB]: "mtr_61Srreh9eWrExDSCe41D3Ee2Ir7Wm5YW"
|
||||
};
|
||||
|
||||
export const FeatureMeterIdsSandbox: Partial<Record<FeatureId, string>> = {
|
||||
export const FeatureMeterIdsSandbox: Partial<Record<LimitId, string>> = {
|
||||
// [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 +55,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<Record<FeatureId, string>>;
|
||||
export type FeaturePriceSet = Partial<Record<LimitId, string>>;
|
||||
|
||||
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 +83,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 +102,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 +120,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 +129,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) {
|
||||
|
||||
@@ -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<Stripe.Checkout.SessionCreateParams.LineItem[]> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,70 +1,82 @@
|
||||
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.ORGANIZATIONS]: { value: 1, description: "Basic limit" },
|
||||
[LimitId.PUBLIC_RESOURCES]: { value: 15, description: "Basic limit" },
|
||||
[LimitId.PRIVATE_RESOURCES]: { value: 15, description: "Basic limit" },
|
||||
[LimitId.MACHINE_CLIENTS]: { value: 5, 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.ORGANIZATIONS]: { value: 1, description: "Home limit" },
|
||||
[LimitId.PUBLIC_RESOURCES]: { value: 30, description: "Home limit" },
|
||||
[LimitId.PRIVATE_RESOURCES]: { value: 30, description: "Home limit" },
|
||||
[LimitId.MACHINE_CLIENTS]: { value: 10, 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.ORGANIZATIONS]: {
|
||||
value: 1,
|
||||
description: "Team limit"
|
||||
}
|
||||
},
|
||||
[LimitId.PUBLIC_RESOURCES]: { value: 150, description: "Team limit" },
|
||||
[LimitId.PRIVATE_RESOURCES]: { value: 150, description: "Team limit" },
|
||||
[LimitId.MACHINE_CLIENTS]: { value: 25, 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.ORGANIZATIONS]: {
|
||||
value: 5,
|
||||
description: "Business limit"
|
||||
},
|
||||
[LimitId.PUBLIC_RESOURCES]: { value: 750, description: "Business limit" },
|
||||
[LimitId.PRIVATE_RESOURCES]: { value: 750, description: "Business limit" },
|
||||
[LimitId.MACHINE_CLIENTS]: { value: 100, description: "Business limit" }
|
||||
};
|
||||
|
||||
@@ -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<number | null> {
|
||||
const limitId = `${orgId}-${featureId}`;
|
||||
const [limit] = await db
|
||||
|
||||
@@ -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<Usage | null> {
|
||||
@@ -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<Usage> {
|
||||
@@ -163,7 +163,7 @@ export class UsageService {
|
||||
|
||||
async updateCount(
|
||||
orgId: string,
|
||||
featureId: FeatureId,
|
||||
featureId: LimitId,
|
||||
value?: number,
|
||||
customerId?: string
|
||||
): Promise<void> {
|
||||
@@ -227,7 +227,7 @@ export class UsageService {
|
||||
|
||||
private async getCustomerId(
|
||||
orgId: string,
|
||||
featureId: FeatureId
|
||||
featureId: LimitId
|
||||
): Promise<string | null> {
|
||||
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<Usage | null> {
|
||||
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<boolean> {
|
||||
@@ -424,7 +424,7 @@ export class UsageService {
|
||||
} else {
|
||||
currentUsage = await this.getUsage(
|
||||
orgIdToUse,
|
||||
limit.featureId as FeatureId,
|
||||
limit.featureId as LimitId,
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
userSiteResources,
|
||||
clientSiteResources
|
||||
} from "@server/db";
|
||||
import { Config, ConfigSchema } from "./types";
|
||||
import { Config, ConfigSchema, isTargetsOnlyResource } from "./types";
|
||||
import {
|
||||
PublicResourcesResults,
|
||||
updatePublicResources
|
||||
@@ -34,6 +34,12 @@ import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
waitForSiteResourceRebuildIdle
|
||||
} from "../rebuildClientAssociations";
|
||||
import { build } from "@server/build";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import next from "next";
|
||||
import { LimitId } from "../billing";
|
||||
import { usageService } from "../billing/usageService";
|
||||
|
||||
type ApplyBlueprintArgs = {
|
||||
orgId: string;
|
||||
@@ -64,6 +70,7 @@ export async function applyBlueprint({
|
||||
|
||||
let publicResourcesResults: PublicResourcesResults = [];
|
||||
let privateResourcesResults: ClientResourcesResults = [];
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await updateResourcePolicies(orgId, config, trx);
|
||||
|
||||
@@ -172,7 +179,9 @@ export async function applyBlueprint({
|
||||
} catch (err) {
|
||||
blueprintSucceeded = false;
|
||||
blueprintMessage = `Blueprint applied with errors: ${err}`;
|
||||
logger.error(blueprintMessage);
|
||||
logger.debug(
|
||||
`Org ${orgId} blueprint apply issues: ${blueprintMessage}`
|
||||
);
|
||||
error = err;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ import { getNextAvailableAliasAddress } from "../ip";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "../billing/tierMatrix";
|
||||
import { build } from "@server/build";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import next from "next";
|
||||
import { LimitId } from "../billing";
|
||||
import { usageService } from "../billing/usageService";
|
||||
|
||||
async function getDomainForSiteResource(
|
||||
siteResourceId: number | undefined,
|
||||
@@ -413,6 +419,34 @@ export async function updatePrivateResources(
|
||||
oldSites: existingSiteIds
|
||||
});
|
||||
} else {
|
||||
// create a brand new resource
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(
|
||||
orgId,
|
||||
LimitId.PRIVATE_RESOURCES
|
||||
);
|
||||
if (!usage) {
|
||||
throw new Error(
|
||||
`Usage data not found for org ${orgId} and limit ${LimitId.PRIVATE_RESOURCES}`
|
||||
);
|
||||
}
|
||||
const rejectResource = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
LimitId.PRIVATE_RESOURCES,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
} // We need to add one to know if we are violating the limit
|
||||
);
|
||||
if (rejectResource) {
|
||||
throw new Error(
|
||||
"Private resource limit exceeded. Please upgrade your plan."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let aliasAddress: string | null = null;
|
||||
let releaseAliasLock: (() => Promise<void>) | null = null;
|
||||
if (
|
||||
@@ -609,6 +643,8 @@ export async function updatePrivateResources(
|
||||
`Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}`
|
||||
);
|
||||
|
||||
await usageService.add(orgId, LimitId.PRIVATE_RESOURCES, 1, trx);
|
||||
|
||||
results.push({
|
||||
newSiteResource: newResource,
|
||||
newSites: allSites,
|
||||
|
||||
@@ -51,6 +51,11 @@ import { build } from "@server/build";
|
||||
import { encrypt } from "@server/lib/crypto";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import serverConfig from "@server/lib/config";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import next from "next";
|
||||
import { LimitId } from "../billing";
|
||||
import { usageService } from "../billing/usageService";
|
||||
|
||||
export type PublicResourcesResults = {
|
||||
proxyResource: Resource;
|
||||
@@ -1005,6 +1010,33 @@ export async function updatePublicResources(
|
||||
logger.debug(`Updated resource ${existingResource.resourceId}`);
|
||||
} else {
|
||||
// create a brand new resource
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(
|
||||
orgId,
|
||||
LimitId.PUBLIC_RESOURCES
|
||||
);
|
||||
if (!usage) {
|
||||
throw new Error(
|
||||
`Usage data not found for org ${orgId} and limit ${LimitId.PUBLIC_RESOURCES}`
|
||||
);
|
||||
}
|
||||
const rejectResource = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
LimitId.PUBLIC_RESOURCES,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
} // We need to add one to know if we are violating the limit
|
||||
);
|
||||
if (rejectResource) {
|
||||
throw new Error(
|
||||
"Public resource limit exceeded. Please upgrade your plan."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let domain;
|
||||
if (
|
||||
["http", "ssh", "rdp", "vnc"].includes(resourceData.mode || "")
|
||||
@@ -1294,6 +1326,8 @@ export async function updatePublicResources(
|
||||
await createTarget(newResource.resourceId, targetData);
|
||||
}
|
||||
|
||||
await usageService.add(orgId, LimitId.PUBLIC_RESOURCES, 1, trx);
|
||||
|
||||
logger.debug(`Created resource ${newResource.resourceId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.ORGANIZATIONS, -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
|
||||
);
|
||||
}
|
||||
|
||||
24
server/lib/orgRebuildCounter.ts
Normal file
24
server/lib/orgRebuildCounter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const ORG_REBUILD_CONCURRENCY_LIMIT = 10;
|
||||
|
||||
const orgActiveRebuilds = new Map<string, number>();
|
||||
|
||||
export async function incrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
orgActiveRebuilds.set(orgId, (orgActiveRebuilds.get(orgId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
export async function decrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
const current = orgActiveRebuilds.get(orgId) ?? 0;
|
||||
if (current <= 1) {
|
||||
orgActiveRebuilds.delete(orgId);
|
||||
} else {
|
||||
orgActiveRebuilds.set(orgId, current - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrgActiveRebuildCount(orgId: string): Promise<number> {
|
||||
return orgActiveRebuilds.get(orgId) ?? 0;
|
||||
}
|
||||
|
||||
export async function checkOrgRebuildRateLimit(orgId: string): Promise<boolean> {
|
||||
return (orgActiveRebuilds.get(orgId) ?? 0) >= ORG_REBUILD_CONCURRENCY_LIMIT;
|
||||
}
|
||||
@@ -45,11 +45,23 @@ import {
|
||||
} from "@server/routers/client/targets";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
import { rebuildQueue } from "#dynamic/lib/rebuildQueue";
|
||||
import {
|
||||
checkOrgRebuildRateLimit,
|
||||
decrementOrgRebuildCount,
|
||||
incrementOrgRebuildCount,
|
||||
ORG_REBUILD_CONCURRENCY_LIMIT
|
||||
} from "#dynamic/lib/orgRebuildCounter";
|
||||
|
||||
export { ORG_REBUILD_CONCURRENCY_LIMIT };
|
||||
|
||||
// TTL for rebuild-association locks. These functions can fan out into many
|
||||
// peer/proxy updates, so give them a generous window.
|
||||
const REBUILD_ASSOCIATIONS_LOCK_TTL_MS = 120000;
|
||||
|
||||
export async function isOrgRebuildRateLimited(orgId: string): Promise<boolean> {
|
||||
return checkOrgRebuildRateLimit(orgId);
|
||||
}
|
||||
|
||||
const REBUILD_IDLE_POLL_INTERVAL_MS = 300;
|
||||
const REBUILD_IDLE_DEFAULT_TIMEOUT_MS = 130_000; // slightly longer than lock TTL
|
||||
const REBUILD_IDLE_HANDLER_TIMEOUT_MS = 5_000;
|
||||
@@ -271,6 +283,7 @@ export async function getClientSiteResourceAccess(
|
||||
export async function rebuildClientAssociationsFromSiteResource(
|
||||
siteResource: SiteResource
|
||||
) {
|
||||
await incrementOrgRebuildCount(siteResource.orgId);
|
||||
try {
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:site-resource:${siteResource.siteResourceId}`,
|
||||
@@ -292,6 +305,8 @@ export async function rebuildClientAssociationsFromSiteResource(
|
||||
return { mergedAllClients: [] };
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
await decrementOrgRebuildCount(siteResource.orgId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1638,8 +1653,9 @@ export async function handleMessagingForUpdatedSiteResource(
|
||||
export async function rebuildClientAssociationsFromClient(
|
||||
client: Client
|
||||
): Promise<void> {
|
||||
const trx = primaryDb;
|
||||
await incrementOrgRebuildCount(client.orgId);
|
||||
try {
|
||||
const trx = primaryDb;
|
||||
return await lockManager.withLock(
|
||||
`rebuild-client-associations:client:${client.clientId}`,
|
||||
() => rebuildClientAssociationsFromClientImpl(client, trx),
|
||||
@@ -1660,6 +1676,8 @@ export async function rebuildClientAssociationsFromClient(
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
} finally {
|
||||
await decrementOrgRebuildCount(client.orgId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
105
server/private/lib/orgRebuildCounter.ts
Normal file
105
server/private/lib/orgRebuildCounter.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { redis } from "#private/lib/redis";
|
||||
import logger from "@server/logger";
|
||||
|
||||
export const ORG_REBUILD_CONCURRENCY_LIMIT = 5;
|
||||
|
||||
// Safety-net TTL: slightly longer than the rebuild lock TTL (120 s). If a
|
||||
// server process dies while holding a rebuild, this ensures the counter key
|
||||
// eventually expires rather than staying inflated forever.
|
||||
const ORG_REBUILD_COUNT_TTL_MS = 180000;
|
||||
const KEY_PREFIX = "rebuild-org-count:";
|
||||
|
||||
// In-memory fallback used when Redis is unavailable.
|
||||
const localFallback = new Map<string, number>();
|
||||
|
||||
function isRedisReady(): boolean {
|
||||
return !!(redis && redis.status === "ready");
|
||||
}
|
||||
|
||||
export async function incrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
if (!isRedisReady()) {
|
||||
localFallback.set(orgId, (localFallback.get(orgId) ?? 0) + 1);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const key = `${KEY_PREFIX}${orgId}`;
|
||||
await redis!.incr(key);
|
||||
// Always refresh the TTL so the key doesn't expire while rebuilds are
|
||||
// still in progress. The TTL is purely a crash-recovery safety net.
|
||||
await redis!.pexpire(key, ORG_REBUILD_COUNT_TTL_MS);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`orgRebuildCounter: Redis increment failed for org ${orgId}, falling back to local:`,
|
||||
err
|
||||
);
|
||||
localFallback.set(orgId, (localFallback.get(orgId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function decrementOrgRebuildCount(orgId: string): Promise<void> {
|
||||
if (!isRedisReady()) {
|
||||
const current = localFallback.get(orgId) ?? 0;
|
||||
if (current <= 1) {
|
||||
localFallback.delete(orgId);
|
||||
} else {
|
||||
localFallback.set(orgId, current - 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const key = `${KEY_PREFIX}${orgId}`;
|
||||
const count = await redis!.decr(key);
|
||||
if (count <= 0) {
|
||||
await redis!.del(key);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`orgRebuildCounter: Redis decrement failed for org ${orgId}, falling back to local:`,
|
||||
err
|
||||
);
|
||||
const current = localFallback.get(orgId) ?? 0;
|
||||
if (current <= 1) {
|
||||
localFallback.delete(orgId);
|
||||
} else {
|
||||
localFallback.set(orgId, current - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getOrgActiveRebuildCount(orgId: string): Promise<number> {
|
||||
if (!isRedisReady()) {
|
||||
return localFallback.get(orgId) ?? 0;
|
||||
}
|
||||
try {
|
||||
const key = `${KEY_PREFIX}${orgId}`;
|
||||
const val = await redis!.get(key);
|
||||
return val ? parseInt(val, 10) : 0;
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
`orgRebuildCounter: Redis get failed for org ${orgId}, falling back to local:`,
|
||||
err
|
||||
);
|
||||
return localFallback.get(orgId) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkOrgRebuildRateLimit(
|
||||
orgId: string
|
||||
): Promise<boolean> {
|
||||
return (
|
||||
(await getOrgActiveRebuildCount(orgId)) >= ORG_REBUILD_CONCURRENCY_LIMIT
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import logger from "@server/logger";
|
||||
import redisManager from "#private/lib/redis";
|
||||
import { regionalRedisManager as redisManager } from "#private/lib/redis";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Rate limiting configuration
|
||||
@@ -152,10 +152,9 @@ export class RateLimitService {
|
||||
);
|
||||
|
||||
// Set TTL using the client directly - this prevents the key from persisting forever
|
||||
if (redisManager.getClient()) {
|
||||
await redisManager
|
||||
.getClient()
|
||||
.expire(globalKey, RATE_LIMIT_WINDOW + 10);
|
||||
const writeClient = redisManager.getClient();
|
||||
if (writeClient) {
|
||||
await writeClient.expire(globalKey, RATE_LIMIT_WINDOW + 10);
|
||||
}
|
||||
|
||||
// Update tracking
|
||||
@@ -204,10 +203,12 @@ export class RateLimitService {
|
||||
);
|
||||
|
||||
// Set TTL using the client directly - this prevents the key from persisting forever
|
||||
if (redisManager.getClient()) {
|
||||
await redisManager
|
||||
.getClient()
|
||||
.expire(messageTypeKey, RATE_LIMIT_WINDOW + 10);
|
||||
const writeClient = redisManager.getClient();
|
||||
if (writeClient) {
|
||||
await writeClient.expire(
|
||||
messageTypeKey,
|
||||
RATE_LIMIT_WINDOW + 10
|
||||
);
|
||||
}
|
||||
|
||||
// Update tracking
|
||||
@@ -487,16 +488,13 @@ export class RateLimitService {
|
||||
await redisManager.del(globalKey);
|
||||
|
||||
// Get all message type keys for this client and delete them
|
||||
const client = redisManager.getClient();
|
||||
if (client) {
|
||||
const messageTypeKeys = await client.keys(
|
||||
`ratelimit:${clientId}:*`
|
||||
const messageTypeKeys = await redisManager.keys(
|
||||
`ratelimit:${clientId}:*`
|
||||
);
|
||||
if (messageTypeKeys.length > 0) {
|
||||
await Promise.all(
|
||||
messageTypeKeys.map((key) => redisManager.del(key))
|
||||
);
|
||||
if (messageTypeKeys.length > 0) {
|
||||
await Promise.all(
|
||||
messageTypeKeys.map((key) => redisManager.del(key))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1000,6 +1000,45 @@ class RegionalRedisManager {
|
||||
}
|
||||
}
|
||||
|
||||
public getClient(): Redis | null {
|
||||
return this.writeClient;
|
||||
}
|
||||
|
||||
public async hget(key: string, field: string): Promise<string | null> {
|
||||
if (!this.isRedisEnabled() || !this.readClient) return null;
|
||||
try {
|
||||
return await this.readClient.hget(key, field);
|
||||
} catch (error) {
|
||||
logger.error("Regional Redis HGET error:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async hset(
|
||||
key: string,
|
||||
field: string,
|
||||
value: string
|
||||
): Promise<boolean> {
|
||||
if (!this.isRedisEnabled() || !this.writeClient) return false;
|
||||
try {
|
||||
await this.writeClient.hset(key, field, value);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error("Regional Redis HSET error:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async hgetall(key: string): Promise<Record<string, string>> {
|
||||
if (!this.isRedisEnabled() || !this.readClient) return {};
|
||||
try {
|
||||
return await this.readClient.hgetall(key);
|
||||
} catch (error) {
|
||||
logger.error("Regional Redis HGETALL error:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
public async disconnect(): Promise<void> {
|
||||
try {
|
||||
if (this.writeClient) {
|
||||
|
||||
@@ -17,3 +17,4 @@ export * from "./queryAccessAuditLog";
|
||||
export * from "./exportAccessAuditLog";
|
||||
export * from "./queryConnectionAuditLog";
|
||||
export * from "./exportConnectionAuditLog";
|
||||
export * from "./logAccessAuditAttempt";
|
||||
|
||||
95
server/private/routers/auditLogs/logAccessAuditAttempt.ts
Normal file
95
server/private/routers/auditLogs/logAccessAuditAttempt.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { NextFunction } from "express";
|
||||
import { Request, Response } from "express";
|
||||
import { z } from "zod";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
||||
|
||||
export const logAccessAuditAttemptSchema = z.object({
|
||||
resourceId: z.number().int().positive(),
|
||||
action: z.boolean(),
|
||||
type: z.enum(["login", "ssh", "vnc", "rdp"])
|
||||
});
|
||||
|
||||
export const logAccessAuditAttemptParams = z.object({
|
||||
orgId: z.string()
|
||||
});
|
||||
|
||||
export async function logAccessAuditAttempt(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedBody = logAccessAuditAttemptSchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const parsedParams = logAccessAuditAttemptParams.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
const { resourceId, action, type } = parsedBody.data;
|
||||
|
||||
const username = req.user?.username;
|
||||
const userId = req.user?.userId;
|
||||
|
||||
await logAccessAudit({
|
||||
orgId: orgId,
|
||||
resourceId: resourceId,
|
||||
action: action,
|
||||
...(username && userId
|
||||
? {
|
||||
user: {
|
||||
username,
|
||||
userId
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
type: type,
|
||||
userAgent: req.headers["user-agent"],
|
||||
requestIp: req.ip
|
||||
});
|
||||
|
||||
return response<null>(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Access audit attempt logged successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { registry } from "@server/openApi";
|
||||
import { NextFunction } from "express";
|
||||
import { Request, Response } from "express";
|
||||
import { eq, gt, lt, and, count, desc, inArray, isNull } from "drizzle-orm";
|
||||
import { eq, gt, lt, and, count, desc, inArray, isNull, or } from "drizzle-orm";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { z } from "zod";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -120,7 +120,10 @@ function getWhere(data: Q) {
|
||||
lt(accessAuditLog.timestamp, data.timeEnd),
|
||||
eq(accessAuditLog.orgId, data.orgId),
|
||||
data.resourceId
|
||||
? eq(accessAuditLog.resourceId, data.resourceId)
|
||||
? or(
|
||||
eq(accessAuditLog.resourceId, data.resourceId),
|
||||
eq(accessAuditLog.siteResourceId, data.resourceId)
|
||||
)
|
||||
: undefined,
|
||||
data.actor ? eq(accessAuditLog.actor, data.actor) : undefined,
|
||||
data.actorType
|
||||
@@ -233,7 +236,6 @@ async function enrichWithResourceDetails(
|
||||
const details = siteResourceMap.get(log.siteResourceId);
|
||||
return {
|
||||
...log,
|
||||
resourceId: log.siteResourceId,
|
||||
resourceName: details?.name ?? null,
|
||||
resourceNiceId: details?.niceId ?? null
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.ORGANIZATIONS
|
||||
);
|
||||
// const egressData = await usageService.getUsage(
|
||||
// orgId,
|
||||
|
||||
@@ -495,29 +495,31 @@ authRouter.post(
|
||||
auth.transferSession
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/license/activate",
|
||||
verifyUserIsServerAdmin,
|
||||
license.activateLicense
|
||||
);
|
||||
if (build !== "saas") {
|
||||
authenticated.post(
|
||||
"/license/activate",
|
||||
verifyUserIsServerAdmin,
|
||||
license.activateLicense
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/license/keys",
|
||||
verifyUserIsServerAdmin,
|
||||
license.listLicenseKeys
|
||||
);
|
||||
authenticated.get(
|
||||
"/license/keys",
|
||||
verifyUserIsServerAdmin,
|
||||
license.listLicenseKeys
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/license/:licenseKey",
|
||||
verifyUserIsServerAdmin,
|
||||
license.deleteLicenseKey
|
||||
);
|
||||
authenticated.delete(
|
||||
"/license/:licenseKey",
|
||||
verifyUserIsServerAdmin,
|
||||
license.deleteLicenseKey
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/license/recheck",
|
||||
verifyUserIsServerAdmin,
|
||||
license.recheckStatus
|
||||
);
|
||||
authenticated.post(
|
||||
"/license/recheck",
|
||||
verifyUserIsServerAdmin,
|
||||
license.recheckStatus
|
||||
);
|
||||
}
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/logs/action",
|
||||
@@ -878,3 +880,9 @@ authenticated.post(
|
||||
verifyClientAccess,
|
||||
client.rebuildClientAssociationsCacheRoute
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/logs/access/attempt",
|
||||
verifyOrgAccess,
|
||||
logs.logAccessAuditAttempt
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { and, eq, sql } from "drizzle-orm";
|
||||
import { removeUserFromOrg } from "@server/lib/userOrg";
|
||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const paramsSchema = z
|
||||
.object({
|
||||
@@ -90,6 +91,15 @@ export async function unassociateOrgIdp(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(org.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgUsersFromIdp = await db
|
||||
.select({
|
||||
userId: userOrgs.userId,
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -23,7 +23,10 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const addUserRoleParamsSchema = z.strictObject({
|
||||
userId: z.string(),
|
||||
@@ -128,6 +131,15 @@ export async function addUserRole(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(role.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let newUserRole: {
|
||||
userId: string;
|
||||
orgId: string;
|
||||
|
||||
@@ -21,7 +21,10 @@ import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const setUserOrgRolesParamsSchema = z.strictObject({
|
||||
orgId: z.string(),
|
||||
@@ -87,6 +90,15 @@ export async function setUserOrgRoles(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const orgRoles = await db
|
||||
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
|
||||
.from(roles)
|
||||
|
||||
@@ -68,6 +68,7 @@ export type QueryAccessAuditLogResponse = {
|
||||
actorType: string | null;
|
||||
actorId: string | null;
|
||||
resourceId: number | null;
|
||||
siteResourceId: number | null;
|
||||
resourceName: string | null;
|
||||
resourceNiceId: string | null;
|
||||
ip: string | null;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -24,9 +24,14 @@ import { isIpInCidr } from "@server/lib/ip";
|
||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import { getUniqueClientName } from "@server/db/names";
|
||||
import { build } from "@server/build";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
|
||||
const createClientParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -125,6 +130,38 @@ export async function createClient(
|
||||
);
|
||||
}
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(
|
||||
orgId,
|
||||
LimitId.MACHINE_CLIENTS
|
||||
);
|
||||
if (!usage) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"No usage data found for this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
const rejectClient = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
LimitId.MACHINE_CLIENTS,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
} // We need to add one to know if we are violating the limit
|
||||
);
|
||||
if (rejectClient) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Machine client limit exceeded. Please upgrade your plan."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
|
||||
|
||||
if (!org) {
|
||||
@@ -154,6 +191,15 @@ export async function createClient(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
|
||||
|
||||
// make sure the subnet is unique
|
||||
@@ -277,6 +323,8 @@ export async function createClient(
|
||||
clientId: newClient.clientId,
|
||||
dateCreated: moment().toISOString()
|
||||
});
|
||||
|
||||
await usageService.add(orgId, LimitId.MACHINE_CLIENTS, 1, trx);
|
||||
});
|
||||
|
||||
if (newClient) {
|
||||
@@ -291,7 +339,7 @@ export async function createClient(
|
||||
data: newClient,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Site created successfully",
|
||||
message: "Client created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -21,7 +21,10 @@ import { isValidIP } from "@server/lib/validators";
|
||||
import { isIpInCidr } from "@server/lib/ip";
|
||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import { getUniqueClientName } from "@server/db/names";
|
||||
|
||||
const paramsSchema = z
|
||||
@@ -146,6 +149,15 @@ export async function createUserClient(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
|
||||
|
||||
// make sure the subnet is unique
|
||||
|
||||
@@ -9,9 +9,14 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import { sendTerminateClient } from "./terminate";
|
||||
import { OlmErrorCodes } from "../olm/error";
|
||||
import { LimitId } from "@server/lib/billing/features";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
|
||||
const deleteClientSchema = z.strictObject({
|
||||
clientId: z.coerce.number().int().positive()
|
||||
@@ -76,6 +81,15 @@ export async function deleteClient(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(client.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Only allow deletion of machine clients (clients without userId)
|
||||
if (client.userId) {
|
||||
return next(
|
||||
@@ -106,6 +120,13 @@ export async function deleteClient(
|
||||
if (!client.userId && client.olmId) {
|
||||
await trx.delete(olms).where(eq(olms.olmId, client.olmId));
|
||||
}
|
||||
|
||||
await usageService.add(
|
||||
deletedClient.orgId,
|
||||
LimitId.MACHINE_CLIENTS,
|
||||
-1,
|
||||
trx
|
||||
);
|
||||
});
|
||||
|
||||
if (deletedClient) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import { rebuildClientAssociationsFromClient, isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
clientId: z.string().transform(Number).pipe(z.int().positive())
|
||||
@@ -60,6 +60,15 @@ export async function rebuildClientAssociationsCacheRoute(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(client.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
rebuildClientAssociationsFromClient(client).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for client ${clientId}: ${e}`
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<DeleteAccountDomainResponse>(res, {
|
||||
|
||||
@@ -910,19 +910,6 @@ unauthenticated.post(
|
||||
);
|
||||
unauthenticated.get("/my-device", verifySessionMiddleware, user.myDevice);
|
||||
|
||||
authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers);
|
||||
authenticated.get("/user/:userId", verifyUserIsServerAdmin, user.adminGetUser);
|
||||
authenticated.post(
|
||||
"/user/:userId/generate-password-reset-code",
|
||||
verifyUserIsServerAdmin,
|
||||
user.adminGeneratePasswordResetCode
|
||||
);
|
||||
authenticated.delete(
|
||||
"/user/:userId",
|
||||
verifyUserIsServerAdmin,
|
||||
user.adminRemoveUser
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/user",
|
||||
verifyOrgAccess,
|
||||
@@ -945,12 +932,6 @@ authenticated.post(
|
||||
authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser);
|
||||
authenticated.get("/org/:orgId/user/:userId/check", org.checkOrgUserAccess);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/2fa",
|
||||
verifyUserIsServerAdmin,
|
||||
user.updateUser2FA
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/users",
|
||||
verifyOrgAccess,
|
||||
@@ -1033,85 +1014,112 @@ authenticated.post(
|
||||
olm.recoverOlmWithFingerprint
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyUserIsServerAdmin,
|
||||
// verifyUserHasAction(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp
|
||||
);
|
||||
if (build !== "saas") {
|
||||
authenticated.put(
|
||||
"/idp/oidc",
|
||||
verifyUserIsServerAdmin,
|
||||
// verifyUserHasAction(ActionsEnum.createIdp),
|
||||
idp.createOidcIdp
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/oidc",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.updateOidcIdp
|
||||
);
|
||||
authenticated.post(
|
||||
"/idp/:idpId/oidc",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.updateOidcIdp
|
||||
);
|
||||
|
||||
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
|
||||
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
|
||||
|
||||
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
||||
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
||||
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.createIdpOrgPolicy
|
||||
);
|
||||
authenticated.put(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.createIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.updateIdpOrgPolicy
|
||||
);
|
||||
authenticated.post(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.updateIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.deleteIdpOrgPolicy
|
||||
);
|
||||
authenticated.delete(
|
||||
"/idp/:idpId/org/:orgId",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.deleteIdpOrgPolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/idp/:idpId/org",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.listIdpOrgPolicies
|
||||
);
|
||||
authenticated.get(
|
||||
"/idp/:idpId/org",
|
||||
verifyUserIsServerAdmin,
|
||||
idp.listIdpOrgPolicies
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-key/:apiKeyId`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.getApiKey
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
`/api-key`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.createRootApiKey
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/api-key/:apiKeyId`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.deleteApiKey
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-keys`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.listRootApiKeys
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-key/:apiKeyId/actions`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.listApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/api-key/:apiKeyId/actions`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.setApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.get("/users", verifyUserIsServerAdmin, user.adminListUsers);
|
||||
|
||||
authenticated.get(
|
||||
"/user/:userId",
|
||||
verifyUserIsServerAdmin,
|
||||
user.adminGetUser
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/generate-password-reset-code",
|
||||
verifyUserIsServerAdmin,
|
||||
user.adminGeneratePasswordResetCode
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/user/:userId",
|
||||
verifyUserIsServerAdmin,
|
||||
user.adminRemoveUser
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/user/:userId/2fa",
|
||||
verifyUserIsServerAdmin,
|
||||
user.updateUser2FA
|
||||
);
|
||||
}
|
||||
|
||||
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
|
||||
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
||||
|
||||
authenticated.get(
|
||||
`/api-key/:apiKeyId`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.getApiKey
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
`/api-key`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.createRootApiKey
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
`/api-key/:apiKeyId`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.deleteApiKey
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-keys`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.listRootApiKeys
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/api-key/:apiKeyId/actions`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.listApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
`/api-key/:apiKeyId/actions`,
|
||||
verifyUserIsServerAdmin,
|
||||
apiKeys.setApiKeyActions
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
`/org/:orgId/api-keys`,
|
||||
|
||||
@@ -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<void> {
|
||||
}
|
||||
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
usageService
|
||||
.checkLimitSet(
|
||||
orgId,
|
||||
FeatureId.EGRESS_DATA_MB,
|
||||
LimitId.EGRESS_DATA_MB,
|
||||
bandwidthUsage
|
||||
)
|
||||
.catch((error: any) => {
|
||||
@@ -247,10 +248,7 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error processing usage for org ${orgId}:`,
|
||||
error
|
||||
);
|
||||
logger.error(`Error processing usage for org ${orgId}:`, error);
|
||||
// Continue with other orgs.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,22 +49,20 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
||||
`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
|
||||
);
|
||||
|
||||
// TODO: IMPLEMENT THE SYNC ON THE NEWT SIDE AND COMMENT THIS BACK IN
|
||||
const [site] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, newt.siteId))
|
||||
.limit(1);
|
||||
|
||||
// const [site] = await db
|
||||
// .select()
|
||||
// .from(sites)
|
||||
// .where(eq(sites.siteId, newt.siteId))
|
||||
// .limit(1);
|
||||
if (!site) {
|
||||
logger.warn(
|
||||
`Newt ping message: site with ID ${newt.siteId} not found`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!site) {
|
||||
// logger.warn(
|
||||
// `Newt ping message: site with ID ${newt.siteId} not found`
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// await sendNewtSyncMessage(newt, site);
|
||||
await sendNewtSyncMessage(newt, site);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -9,7 +9,10 @@ import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import { sendTerminateClient } from "../client/terminate";
|
||||
import { OlmErrorCodes } from "./error";
|
||||
|
||||
@@ -64,6 +67,30 @@ export async function deleteUserOlm(
|
||||
|
||||
const { olmId } = parsedParams.data;
|
||||
|
||||
// get the client first
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(eq(clients.olmId, olmId));
|
||||
|
||||
if (!client) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`No client found for olmId ${olmId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(client.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let deletedClient: Client | undefined;
|
||||
// Delete associated clients and the OLM in a transaction
|
||||
await db.transaction(async (trx) => {
|
||||
|
||||
@@ -197,15 +197,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
policyCheck
|
||||
});
|
||||
|
||||
if (policyCheck?.error) {
|
||||
logger.error(
|
||||
`[handleOlmRegisterMessage] Error checking access policies for olm user ${olm.userId} in org ${orgId}: ${policyCheck?.error}`,
|
||||
{ orgId: client.orgId, clientId: client.clientId }
|
||||
);
|
||||
sendOlmError(OlmErrorCodes.ORG_ACCESS_POLICY_DENIED, olm.olmId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (policyCheck.policies?.passwordAge?.compliant === false) {
|
||||
logger.warn(
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} has non-compliant password age for org ${orgId}`,
|
||||
@@ -238,7 +229,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
olm.olmId
|
||||
);
|
||||
return;
|
||||
} else if (!policyCheck.allowed) {
|
||||
} else if (!policyCheck.allowed || policyCheck.error) {
|
||||
logger.warn(
|
||||
`[handleOlmRegisterMessage] Olm user ${olm.userId} does not pass access policies for org ${orgId}: ${policyCheck.error}`,
|
||||
{ orgId: client.orgId, clientId: client.clientId }
|
||||
|
||||
@@ -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.ORGANIZATIONS
|
||||
);
|
||||
if (!usage) {
|
||||
return next(
|
||||
@@ -214,7 +214,7 @@ export async function createOrg(
|
||||
}
|
||||
const rejectOrgs = await usageService.checkLimitSet(
|
||||
billingOrgIdForNewOrg,
|
||||
FeatureId.ORGINIZATIONS,
|
||||
LimitId.ORGANIZATIONS,
|
||||
{
|
||||
...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.ORGANIZATIONS,
|
||||
numOrgs
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,6 +76,15 @@ export async function setResourcePolicyHeaderAuth(
|
||||
const { resourcePolicyId } = parsedParams.data;
|
||||
const { headerAuth } = parsedBody.data;
|
||||
|
||||
const headerAuthHash =
|
||||
headerAuth !== null
|
||||
? await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuth.user}:${headerAuth.password}`
|
||||
).toString("base64")
|
||||
)
|
||||
: null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(resourcePolicyHeaderAuth)
|
||||
@@ -86,13 +95,7 @@ export async function setResourcePolicyHeaderAuth(
|
||||
)
|
||||
);
|
||||
|
||||
if (headerAuth !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(
|
||||
`${headerAuth.user}:${headerAuth.password}`
|
||||
).toString("base64")
|
||||
);
|
||||
|
||||
if (headerAuth !== null && headerAuthHash !== null) {
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId,
|
||||
headerAuthHash,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, resourcePolicyRules, resourcePolicies } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq, notInArray } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
|
||||
const ruleSchema = z.strictObject({
|
||||
ruleId: z.int().positive().optional(),
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({
|
||||
type: "string",
|
||||
enum: ["ACCEPT", "DROP", "PASS"],
|
||||
@@ -121,17 +122,74 @@ export async function setResourcePolicyRules(
|
||||
.set({ applyRules })
|
||||
.where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId));
|
||||
|
||||
await trx
|
||||
.delete(resourcePolicyRules)
|
||||
.where(
|
||||
eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId)
|
||||
);
|
||||
const incomingRuleIds = rules
|
||||
.map((r) => r.ruleId)
|
||||
.filter((id): id is number => id !== undefined);
|
||||
|
||||
if (rules.length > 0) {
|
||||
// Delete rules that are no longer in the incoming list
|
||||
if (incomingRuleIds.length > 0) {
|
||||
await trx
|
||||
.delete(resourcePolicyRules)
|
||||
.where(
|
||||
and(
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
),
|
||||
notInArray(
|
||||
resourcePolicyRules.ruleId,
|
||||
incomingRuleIds
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
await trx
|
||||
.delete(resourcePolicyRules)
|
||||
.where(
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Update existing rules (those with a ruleId)
|
||||
const existingRules = rules.filter(
|
||||
(r): r is typeof r & { ruleId: number } =>
|
||||
r.ruleId !== undefined
|
||||
);
|
||||
for (const rule of existingRules) {
|
||||
await trx
|
||||
.update(resourcePolicyRules)
|
||||
.set({
|
||||
action: rule.action,
|
||||
match: rule.match,
|
||||
value: rule.value,
|
||||
priority: rule.priority,
|
||||
enabled: rule.enabled
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(resourcePolicyRules.ruleId, rule.ruleId),
|
||||
eq(
|
||||
resourcePolicyRules.resourcePolicyId,
|
||||
resourcePolicyId
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Insert new rules (those without a ruleId)
|
||||
const newRules = rules.filter((r) => r.ruleId === undefined);
|
||||
if (newRules.length > 0) {
|
||||
await trx.insert(resourcePolicyRules).values(
|
||||
rules.map((rule) => ({
|
||||
newRules.map((rule) => ({
|
||||
resourcePolicyId,
|
||||
...rule
|
||||
action: rule.action,
|
||||
match: rule.match,
|
||||
value: rule.value,
|
||||
priority: rule.priority,
|
||||
enabled: rule.enabled
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
getUniqueResourceName,
|
||||
getUniqueResourcePolicyName
|
||||
} from "@server/db/names";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
|
||||
const createResourceParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -235,6 +237,38 @@ export async function createResource(
|
||||
req.body.mode = resolvedMode.mode;
|
||||
}
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(
|
||||
orgId,
|
||||
LimitId.PUBLIC_RESOURCES
|
||||
);
|
||||
if (!usage) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"No usage data found for this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
const rejectResource = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
LimitId.PUBLIC_RESOURCES,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
} // We need to add one to know if we are violating the limit
|
||||
);
|
||||
if (rejectResource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Public resource limit exceeded. Please upgrade your plan."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof req.body.proxyPort === "number") {
|
||||
if (
|
||||
!config.getRawConfig().flags?.allow_raw_resources &&
|
||||
@@ -503,6 +537,8 @@ async function createHttpResource(
|
||||
}
|
||||
|
||||
resource = newResource[0];
|
||||
|
||||
await usageService.add(orgId, LimitId.PUBLIC_RESOURCES, 1, trx);
|
||||
});
|
||||
|
||||
if (!resource) {
|
||||
@@ -631,6 +667,8 @@ async function createRawResource(
|
||||
}
|
||||
|
||||
resource = newResource[0];
|
||||
|
||||
await usageService.add(orgId, LimitId.PUBLIC_RESOURCES, 1, trx);
|
||||
});
|
||||
|
||||
if (!resource) {
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
performDeleteResource,
|
||||
runResourceDeleteSideEffects
|
||||
} from "@server/lib/deleteResource";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
|
||||
const deleteResourceSchema = z.strictObject({
|
||||
resourceId: z.coerce.number().int().positive()
|
||||
@@ -64,6 +66,14 @@ export async function deleteResource(
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
deleteResult = await performDeleteResource(resourceId, trx);
|
||||
if (deleteResult?.deletedResource?.orgId) {
|
||||
await usageService.add(
|
||||
deleteResult?.deletedResource?.orgId,
|
||||
LimitId.PUBLIC_RESOURCES,
|
||||
-1,
|
||||
trx
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!deleteResult) {
|
||||
|
||||
@@ -107,6 +107,13 @@ export async function setResourceHeaderAuth(
|
||||
resource.resourcePolicyId === null &&
|
||||
resource.defaultResourcePolicyId !== null;
|
||||
|
||||
const headerAuthHash =
|
||||
user && password && extendedCompatibility !== null
|
||||
? await hashPassword(
|
||||
Buffer.from(`${user}:${password}`).toString("base64")
|
||||
)
|
||||
: null;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
if (isInlinePolicy) {
|
||||
const policyId = resource.defaultResourcePolicyId!;
|
||||
@@ -116,11 +123,7 @@ export async function setResourceHeaderAuth(
|
||||
eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId)
|
||||
);
|
||||
|
||||
if (user && password && extendedCompatibility !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(`${user}:${password}`).toString("base64")
|
||||
);
|
||||
|
||||
if (headerAuthHash !== null && extendedCompatibility !== null) {
|
||||
await trx.insert(resourcePolicyHeaderAuth).values({
|
||||
resourcePolicyId: policyId,
|
||||
headerAuthHash,
|
||||
@@ -140,11 +143,7 @@ export async function setResourceHeaderAuth(
|
||||
)
|
||||
);
|
||||
|
||||
if (user && password && extendedCompatibility !== null) {
|
||||
const headerAuthHash = await hashPassword(
|
||||
Buffer.from(`${user}:${password}`).toString("base64")
|
||||
);
|
||||
|
||||
if (headerAuthHash !== null && extendedCompatibility !== null) {
|
||||
await Promise.all([
|
||||
trx
|
||||
.insert(resourceHeaderAuth)
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -8,7 +8,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const addClientToSiteResourceBodySchema = z
|
||||
.object({
|
||||
@@ -128,6 +131,15 @@ export async function addClientToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if client already exists in site resource
|
||||
const existingEntry = await db
|
||||
.select()
|
||||
|
||||
@@ -9,7 +9,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const addRoleToSiteResourceBodySchema = z
|
||||
.object({
|
||||
@@ -104,6 +107,15 @@ export async function addRoleToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// verify the role exists and belongs to the same org
|
||||
const [role] = await db
|
||||
.select()
|
||||
|
||||
@@ -9,7 +9,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const addUserToSiteResourceBodySchema = z
|
||||
.object({
|
||||
@@ -104,6 +107,15 @@ export async function addUserToSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists in site resource
|
||||
const existingEntry = await db
|
||||
.select()
|
||||
|
||||
@@ -15,7 +15,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const batchAddClientToSiteResourcesParamsSchema = z
|
||||
.object({
|
||||
@@ -186,6 +189,15 @@ export async function batchAddClientToSiteResources(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(client.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (client.userId !== null) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
} from "@server/lib/ip";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
@@ -34,6 +37,8 @@ import { fromError } from "zod-validation-error";
|
||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { build } from "@server/build";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
|
||||
const createSiteResourceParamsSchema = z.strictObject({
|
||||
orgId: z.string()
|
||||
@@ -291,6 +296,38 @@ export async function createSiteResource(
|
||||
siteIds.push(siteId);
|
||||
}
|
||||
|
||||
if (build == "saas") {
|
||||
const usage = await usageService.getUsage(
|
||||
orgId,
|
||||
LimitId.PRIVATE_RESOURCES
|
||||
);
|
||||
if (!usage) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
"No usage data found for this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
const rejectResource = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
LimitId.PRIVATE_RESOURCES,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
} // We need to add one to know if we are violating the limit
|
||||
);
|
||||
if (rejectResource) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"Private resource limit exceeded. Please upgrade your plan."
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == "http") {
|
||||
const hasHttpFeature = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
@@ -339,6 +376,15 @@ export async function createSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(org.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Only check if destination is an IP address
|
||||
const isIp = z
|
||||
.union([z.ipv4(), z.ipv6()])
|
||||
@@ -593,6 +639,13 @@ export async function createSiteResource(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await usageService.add(
|
||||
orgId,
|
||||
LimitId.PRIVATE_RESOURCES,
|
||||
1,
|
||||
trx
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
await releaseAliasLock?.();
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
performDeleteSiteResource,
|
||||
runSiteResourceDeleteSideEffects
|
||||
} from "@server/lib/deleteSiteResource";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
|
||||
const deleteSiteResourceParamsSchema = z.strictObject({
|
||||
siteResourceId: z.coerce.number().int().positive()
|
||||
@@ -86,6 +88,14 @@ export async function deleteSiteResource(
|
||||
siteResourceId,
|
||||
trx
|
||||
);
|
||||
if (removedSiteResource?.orgId) {
|
||||
await usageService.add(
|
||||
removedSiteResource?.orgId,
|
||||
LimitId.PRIVATE_RESOURCES,
|
||||
-1,
|
||||
trx
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!removedSiteResource) {
|
||||
|
||||
@@ -8,7 +8,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const removeClientFromSiteResourceBodySchema = z
|
||||
.object({
|
||||
@@ -106,6 +109,14 @@ export async function removeClientFromSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
// Check if client exists and has a userId
|
||||
const [client] = await db
|
||||
.select()
|
||||
|
||||
@@ -9,7 +9,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const removeRoleFromSiteResourceBodySchema = z
|
||||
.object({
|
||||
@@ -106,6 +109,15 @@ export async function removeRoleFromSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the role is an admin role
|
||||
const [roleToCheck] = await db
|
||||
.select()
|
||||
|
||||
@@ -9,7 +9,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const removeUserFromSiteResourceBodySchema = z
|
||||
.object({
|
||||
@@ -106,6 +109,15 @@ export async function removeUserFromSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user exists in site resource
|
||||
const existingEntry = await db
|
||||
.select()
|
||||
|
||||
@@ -8,7 +8,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const setSiteResourceClientsBodySchema = z
|
||||
.object({
|
||||
@@ -107,6 +110,15 @@ export async function setSiteResourceClients(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if any clients have a userId (associated with a user)
|
||||
if (clientIds.length > 0) {
|
||||
const clientsWithUsers = await db
|
||||
|
||||
@@ -9,7 +9,7 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and, ne, inArray } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import { rebuildClientAssociationsFromSiteResource, isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const setSiteResourceRolesBodySchema = z
|
||||
.object({
|
||||
@@ -167,6 +167,15 @@ export async function setSiteResourceRoles(
|
||||
}
|
||||
});
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
rebuildClientAssociationsFromSiteResource(siteResource).catch((e) => {
|
||||
logger.error(
|
||||
`Failed to rebuild client associations for site resource ${siteResourceId}. Error: ${e}`
|
||||
|
||||
@@ -9,7 +9,10 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
import { error } from "node:console";
|
||||
|
||||
const setSiteResourceUsersBodySchema = z
|
||||
@@ -109,6 +112,15 @@ export async function setSiteResourceUsers(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(siteResource.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
await trx
|
||||
.delete(userSiteResources)
|
||||
|
||||
@@ -19,6 +19,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { isIpInCidr, portRangeStringSchema } from "@server/lib/ip";
|
||||
import {
|
||||
handleMessagingForUpdatedSiteResource,
|
||||
isOrgRebuildRateLimited,
|
||||
rebuildClientAssociationsFromSiteResource,
|
||||
waitForSiteResourceRebuildIdle
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
@@ -345,6 +346,15 @@ export async function updateSiteResource(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(org.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the site exists and belongs to the org
|
||||
const sitesToAssign = await db
|
||||
.select()
|
||||
|
||||
@@ -17,10 +17,11 @@ 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";
|
||||
import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const acceptInviteBodySchema = z.strictObject({
|
||||
token: z.string(),
|
||||
@@ -103,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(
|
||||
@@ -116,7 +117,7 @@ export async function acceptInvite(
|
||||
const rejectUsers = await usageService.checkLimitSet(
|
||||
existingInvite.orgId,
|
||||
|
||||
FeatureId.USERS,
|
||||
LimitId.USERS,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
@@ -147,6 +148,15 @@ export async function acceptInvite(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(org.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const inviteRoleRows = await db
|
||||
.select({ roleId: userInviteRoles.roleId })
|
||||
.from(userInviteRoles)
|
||||
|
||||
@@ -10,7 +10,10 @@ import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
|
||||
import {
|
||||
rebuildClientAssociationsFromClient,
|
||||
isOrgRebuildRateLimited
|
||||
} from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
/** Legacy path param order: /role/:roleId/add/:userId */
|
||||
const addUserRoleLegacyParamsSchema = z.strictObject({
|
||||
@@ -127,6 +130,15 @@ export async function addUserRoleLegacy(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(role.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let orgClientsToRebuild: Client[] = [];
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
|
||||
@@ -12,13 +12,14 @@ 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";
|
||||
import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { assignUserToOrg } from "@server/lib/userOrg";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
@@ -122,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(
|
||||
@@ -134,7 +135,7 @@ export async function createOrgUser(
|
||||
const rejectUsers = await usageService.checkLimitSet(
|
||||
orgId,
|
||||
|
||||
FeatureId.USERS,
|
||||
LimitId.USERS,
|
||||
{
|
||||
...usage,
|
||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||
@@ -229,6 +230,15 @@ export async function createOrgUser(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(org.orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [idpRes] = await db
|
||||
.select()
|
||||
.from(idp)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,29 +1,17 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
db,
|
||||
orgs,
|
||||
resources,
|
||||
siteResources,
|
||||
sites,
|
||||
UserOrg,
|
||||
userSiteResources,
|
||||
primaryDb
|
||||
} from "@server/db";
|
||||
import { userOrgs, userResources, users, userSites } from "@server/db";
|
||||
import { and, count, eq, exists, inArray } from "drizzle-orm";
|
||||
import { db, orgs } from "@server/db";
|
||||
import { userOrgs } from "@server/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import response from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { FeatureId } from "@server/lib/billing";
|
||||
import { build } from "@server/build";
|
||||
import { UserType } from "@server/types/UserTypes";
|
||||
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
|
||||
import { removeUserFromOrg } from "@server/lib/userOrg";
|
||||
import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
|
||||
|
||||
const removeUserSchema = z.strictObject({
|
||||
userId: z.string(),
|
||||
@@ -93,6 +81,15 @@ export async function removeUserOrg(
|
||||
);
|
||||
}
|
||||
|
||||
if (await isOrgRebuildRateLimited(orgId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.TOO_MANY_REQUESTS,
|
||||
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [org] = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
|
||||
@@ -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.ORGANIZATIONS]?.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.ORGANIZATIONS]?.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.ORGANIZATIONS]?.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.ORGANIZATIONS]?.value ?? 0
|
||||
},
|
||||
enterprise: {
|
||||
users: 0, // Custom for enterprise
|
||||
|
||||
@@ -342,7 +342,7 @@ export default function GeneralPage() {
|
||||
return (
|
||||
<Link
|
||||
href={
|
||||
row.original.type === "ssh"
|
||||
row.original.siteResourceId != null
|
||||
? `/${row.original.orgId}/settings/resources/private?query=${row.original.resourceNiceId}`
|
||||
: `/${row.original.orgId}/settings/resources/public/${row.original.resourceNiceId}`
|
||||
}
|
||||
@@ -369,7 +369,9 @@ export default function GeneralPage() {
|
||||
value: "whitelistedEmail",
|
||||
label: "Whitelisted Email"
|
||||
},
|
||||
{ value: "ssh", label: "SSH" }
|
||||
{ value: "ssh", label: "SSH" },
|
||||
{ value: "rdp", label: "RDP" },
|
||||
{ value: "vnc", label: "VNC" }
|
||||
]}
|
||||
label={t("type")}
|
||||
selectedValue={filters.type}
|
||||
@@ -384,8 +386,10 @@ export default function GeneralPage() {
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const typeLabel =
|
||||
row.original.type === "ssh"
|
||||
? "SSH"
|
||||
row.original.type === "ssh" ||
|
||||
row.original.type === "rdp" ||
|
||||
row.original.type === "vnc"
|
||||
? row.original.type.toUpperCase()
|
||||
: row.original.type.charAt(0).toUpperCase() +
|
||||
row.original.type.slice(1);
|
||||
return <span>{typeLabel || "-"}</span>;
|
||||
@@ -513,7 +517,15 @@ export default function GeneralPage() {
|
||||
|
||||
function generateSampleAccessLogs(): QueryAccessAuditLogResponse["log"] {
|
||||
const locations = ["US", "DE", "GB", "FR", "JP", "CA", "AU"];
|
||||
const types = ["password", "pincode", "login", "whitelistedEmail", "ssh"];
|
||||
const types = [
|
||||
"password",
|
||||
"pincode",
|
||||
"login",
|
||||
"whitelistedEmail",
|
||||
"ssh",
|
||||
"rdp",
|
||||
"vnc"
|
||||
];
|
||||
const actors = [
|
||||
"alice@example.com",
|
||||
"bob@example.com",
|
||||
@@ -538,6 +550,7 @@ function generateSampleAccessLogs(): QueryAccessAuditLogResponse["log"] {
|
||||
actor,
|
||||
actorId: actor ? `user-${i}` : null,
|
||||
resourceId: Math.floor(Math.random() * 5) + 1,
|
||||
siteResourceId: null,
|
||||
resourceNiceId: `resource-${(i % 3) + 1}`,
|
||||
resourceName: `Resource ${(i % 3) + 1}`,
|
||||
ip: `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Layout } from "@app/components/Layout";
|
||||
import { adminNavSections } from "../navigation";
|
||||
import { pullEnv } from "@app/lib/pullEnv";
|
||||
import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
|
||||
import { build } from "@server/build";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -29,6 +30,11 @@ export default async function AdminLayout(props: LayoutProps) {
|
||||
const getUser = cache(verifySession);
|
||||
const user = await getUser();
|
||||
|
||||
// Disable the admin page on saas
|
||||
if (build == "saas") {
|
||||
redirect(`/`);
|
||||
}
|
||||
|
||||
const env = pullEnv();
|
||||
|
||||
if (!user || !user.serverAdmin) {
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
loadEncryptedLocalStorage,
|
||||
saveEncryptedLocalStorage
|
||||
} from "@app/lib/secureLocalStorage";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
declare module "react" {
|
||||
namespace JSX {
|
||||
@@ -96,6 +98,7 @@ export default function RdpClient({
|
||||
primaryColor?: string | null;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const STORAGE_KEY = "pangolin_rdp_credentials";
|
||||
const resourceName = target?.name?.trim() || null;
|
||||
|
||||
@@ -311,6 +314,11 @@ export default function RdpClient({
|
||||
values,
|
||||
target.authToken
|
||||
);
|
||||
void api.post(`/org/${target.orgId}/logs/access/attempt`, {
|
||||
resourceId: target.resourceId,
|
||||
action: true,
|
||||
type: "rdp"
|
||||
});
|
||||
setConnecting(false);
|
||||
setShowLogin(false);
|
||||
userInteraction.setVisibility(true);
|
||||
@@ -320,6 +328,11 @@ export default function RdpClient({
|
||||
fileTransferRef.current = null;
|
||||
setShowLogin(true);
|
||||
} catch (err) {
|
||||
void api.post(`/org/${target.orgId}/logs/access/attempt`, {
|
||||
resourceId: target.resourceId,
|
||||
action: false,
|
||||
type: "rdp"
|
||||
});
|
||||
setConnecting(false);
|
||||
setShowLogin(true);
|
||||
if (isIronError(err)) {
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
loadEncryptedLocalStorage,
|
||||
saveEncryptedLocalStorage
|
||||
} from "@app/lib/secureLocalStorage";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
type AuthTab = "password" | "privateKey";
|
||||
|
||||
@@ -73,6 +75,7 @@ export default function SshClient({
|
||||
}) {
|
||||
const STORAGE_KEY = "pangolin_ssh_credentials";
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const resourceName = target?.name?.trim() || null;
|
||||
|
||||
const passwordTabSchema = z.object({
|
||||
@@ -263,6 +266,17 @@ export default function SshClient({
|
||||
let authConfirmed = false;
|
||||
let authErrorShown = false;
|
||||
let socketOpened = false;
|
||||
let auditLogged = false;
|
||||
|
||||
const logAudit = (action: boolean) => {
|
||||
if (auditLogged || !target) return;
|
||||
auditLogged = true;
|
||||
void api.post(`/org/${target.orgId}/logs/access/attempt`, {
|
||||
resourceId: target.resourceId,
|
||||
action,
|
||||
type: "ssh"
|
||||
});
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
socketOpened = true;
|
||||
@@ -294,6 +308,7 @@ export default function SshClient({
|
||||
if (msg.type === "data" && msg.data) {
|
||||
if (!authConfirmed) {
|
||||
authConfirmed = true;
|
||||
logAudit(true);
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
}
|
||||
@@ -301,6 +316,7 @@ export default function SshClient({
|
||||
} else if (msg.type === "error") {
|
||||
if (!authConfirmed) {
|
||||
authErrorShown = true;
|
||||
logAudit(false);
|
||||
setConnecting(false);
|
||||
setConnectError(
|
||||
msg.error ?? t("sshErrorAuthFailed")
|
||||
@@ -323,6 +339,7 @@ export default function SshClient({
|
||||
evt.data.text().then((text) => {
|
||||
if (!authConfirmed) {
|
||||
authConfirmed = true;
|
||||
logAudit(true);
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
}
|
||||
@@ -332,6 +349,7 @@ export default function SshClient({
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
logAudit(false);
|
||||
setConnecting(false);
|
||||
setConnected(false);
|
||||
setConnectError(t("sshErrorWebSocket"));
|
||||
@@ -355,6 +373,7 @@ export default function SshClient({
|
||||
);
|
||||
}
|
||||
if (!authConfirmed && !authErrorShown) {
|
||||
logAudit(false);
|
||||
setConnectError(t("sshErrorConnectionClosed"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
loadEncryptedLocalStorage,
|
||||
saveEncryptedLocalStorage
|
||||
} from "@app/lib/secureLocalStorage";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
type VncCredentialsForm = {
|
||||
password: string;
|
||||
@@ -52,6 +54,7 @@ export default function VncClient({
|
||||
primaryColor?: string | null;
|
||||
}) {
|
||||
const t = useTranslations();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const STORAGE_KEY = "pangolin_vnc_credentials";
|
||||
const resourceName = target?.name?.trim() || null;
|
||||
|
||||
@@ -179,6 +182,7 @@ export default function VncClient({
|
||||
}
|
||||
|
||||
let authConfirmed = false;
|
||||
let auditLogged = false;
|
||||
|
||||
rfb.scaleViewport = true;
|
||||
rfb.resizeSession = true;
|
||||
@@ -190,6 +194,12 @@ export default function VncClient({
|
||||
target.authToken
|
||||
);
|
||||
authConfirmed = true;
|
||||
auditLogged = true;
|
||||
void api.post(`/org/${target.orgId}/logs/access/attempt`, {
|
||||
resourceId: target.resourceId,
|
||||
action: true,
|
||||
type: "vnc"
|
||||
});
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
});
|
||||
@@ -201,6 +211,17 @@ export default function VncClient({
|
||||
setConnecting(false);
|
||||
setConnected(false);
|
||||
if (!authConfirmed && !e.detail.clean) {
|
||||
if (!auditLogged) {
|
||||
auditLogged = true;
|
||||
void api.post(
|
||||
`/org/${target.orgId}/logs/access/attempt`,
|
||||
{
|
||||
resourceId: target.resourceId,
|
||||
action: false,
|
||||
type: "vnc"
|
||||
}
|
||||
);
|
||||
}
|
||||
setConnectError(t("sshErrorConnectionClosed"));
|
||||
}
|
||||
}
|
||||
@@ -209,6 +230,12 @@ export default function VncClient({
|
||||
rfb.addEventListener(
|
||||
"securityfailure",
|
||||
(e: { detail: { status: number; reason?: string } }) => {
|
||||
auditLogged = true;
|
||||
void api.post(`/org/${target.orgId}/logs/access/attempt`, {
|
||||
resourceId: target.resourceId,
|
||||
action: false,
|
||||
type: "vnc"
|
||||
});
|
||||
disconnect();
|
||||
setConnectError(
|
||||
e.detail.reason ??
|
||||
|
||||
@@ -90,7 +90,11 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<span className="inline-flex items-center">
|
||||
{resource.ssl ? "HTTPS" : "HTTP"}
|
||||
{resource.mode == "http"
|
||||
? resource.ssl
|
||||
? "HTTPS"
|
||||
: "HTTP"
|
||||
: resource.mode?.toUpperCase()}
|
||||
</span>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
<InfoSectionTitle>{t("publicIpEndpoint")}</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{formatPublicEndpoint(site.endpoint)}
|
||||
<span className="text-lg">
|
||||
<span>
|
||||
{site.countryCode &&
|
||||
countryCodeToFlagEmoji(site.countryCode)}
|
||||
</span>
|
||||
|
||||
@@ -340,7 +340,8 @@ function PolicyAccessRulesSectionEdit({
|
||||
? rules.filter((rule) => !rule.fromPolicy)
|
||||
: rules;
|
||||
const rulesPayload = rulesToValidate.map(
|
||||
({ action, match, value, priority, enabled }) => ({
|
||||
({ ruleId, action, match, value, priority, enabled, new: isNew }) => ({
|
||||
...(isNew ? {} : { ruleId }),
|
||||
action,
|
||||
match,
|
||||
value,
|
||||
|
||||
Reference in New Issue
Block a user