mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-12 17:02:08 +02:00
Merge branch 'dev' into feat/geoip-country-is-not-rule
This commit is contained in:
@@ -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, {
|
||||
|
||||
+154
-89
@@ -17,6 +17,7 @@ import * as idp from "./idp";
|
||||
import * as blueprints from "./blueprints";
|
||||
import * as apiKeys from "./apiKeys";
|
||||
import * as logs from "./auditLogs";
|
||||
import * as launcher from "./launcher";
|
||||
import * as newt from "./newt";
|
||||
import * as olm from "./olm";
|
||||
import * as serverInfo from "./serverInfo";
|
||||
@@ -254,6 +255,14 @@ authenticated.delete(
|
||||
site.deleteSite
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/site/:siteId/restart",
|
||||
verifySiteAccess,
|
||||
verifyUserHasAction(ActionsEnum.restartSite),
|
||||
logActionAudit(ActionsEnum.restartSite),
|
||||
site.restartSite
|
||||
);
|
||||
|
||||
// TODO: BREAK OUT THESE ACTIONS SO THEY ARE NOT ALL "getSite"
|
||||
authenticated.get(
|
||||
"/site/:siteId/docker/status",
|
||||
@@ -455,6 +464,54 @@ authenticated.get(
|
||||
resource.getUserResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/groups",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherGroups
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/resources",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherResources
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/sites",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherSites
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/labels",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherLabels
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/launcher/views",
|
||||
verifyOrgAccess,
|
||||
launcher.listLauncherViews
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/launcher/views",
|
||||
verifyOrgAccess,
|
||||
launcher.createLauncherView
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/org/:orgId/launcher/views/:viewId",
|
||||
verifyOrgAccess,
|
||||
launcher.updateLauncherView
|
||||
);
|
||||
|
||||
authenticated.delete(
|
||||
"/org/:orgId/launcher/views/:viewId",
|
||||
verifyOrgAccess,
|
||||
launcher.deleteLauncherView
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/user-resource-aliases",
|
||||
verifyOrgAccess,
|
||||
@@ -910,19 +967,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 +989,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 +1071,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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ authenticated.get(
|
||||
verifyApiKeyOrgAccess,
|
||||
resource.getUserResources
|
||||
);
|
||||
|
||||
// Site Resource endpoints
|
||||
authenticated.put(
|
||||
"/org/:orgId/site-resource",
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import moment from "moment";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const createLauncherViewBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(128),
|
||||
config: launcherViewConfigSchema,
|
||||
orgWide: z.boolean().optional().default(false)
|
||||
});
|
||||
|
||||
export async function createLauncherView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = createLauncherViewBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.orgWide) {
|
||||
const canCreateOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
if (!canCreateOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const now = moment().toISOString();
|
||||
const [created] = await db
|
||||
.insert(launcherViews)
|
||||
.values({
|
||||
orgId,
|
||||
userId: parsed.data.orgWide ? null : userId,
|
||||
name: parsed.data.name,
|
||||
config: JSON.stringify(parsed.data.config),
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
})
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
viewId: created.viewId,
|
||||
orgId: created.orgId,
|
||||
userId: created.userId,
|
||||
name: created.name,
|
||||
config: launcherViewConfigSchema.parse(
|
||||
JSON.parse(created.config)
|
||||
),
|
||||
createdAt: created.createdAt,
|
||||
updatedAt: created.updatedAt,
|
||||
isOrgWide: created.userId == null
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher view created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error creating launcher view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import { getFirstString } from "@server/lib/requestParams";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
|
||||
export async function deleteLauncherView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
const viewId = Number.parseInt(
|
||||
getFirstString(req.params.viewId) ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
if (!orgId || !Number.isFinite(viewId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid request parameters"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.viewId, viewId),
|
||||
eq(launcherViews.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
|
||||
);
|
||||
}
|
||||
|
||||
const isPersonalView = existing.userId === userId;
|
||||
const isOrgWideView = existing.userId == null;
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
|
||||
if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You do not have permission to delete this view"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db.delete(launcherViews).where(eq(launcherViews.viewId, viewId));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher view deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error deleting launcher view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { formatEndpoint, parseEndpoint } from "@server/lib/ip";
|
||||
|
||||
export type SiteResourceDestinationInput = {
|
||||
mode: "host" | "cidr" | "http" | "ssh";
|
||||
destination: string | null;
|
||||
destinationPort: number | null;
|
||||
scheme: "http" | "https" | null;
|
||||
};
|
||||
|
||||
export function resolveHttpHttpsDisplayPort(
|
||||
mode: "http",
|
||||
destinationPort: number | null
|
||||
): number {
|
||||
if (destinationPort != null) {
|
||||
return destinationPort;
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
export function formatSiteResourceDestinationDisplay(
|
||||
row: SiteResourceDestinationInput
|
||||
): string {
|
||||
if (!row.destination) {
|
||||
return "";
|
||||
}
|
||||
const { mode, destination, destinationPort, scheme } = row;
|
||||
if (mode !== "http") {
|
||||
return destination;
|
||||
}
|
||||
const port = resolveHttpHttpsDisplayPort(mode, destinationPort);
|
||||
const downstreamScheme = scheme ?? "http";
|
||||
const hostPart =
|
||||
destination.includes(":") && !destination.startsWith("[")
|
||||
? `[${destination}]`
|
||||
: destination;
|
||||
return `${downstreamScheme}://${hostPart}:${port}`;
|
||||
}
|
||||
|
||||
export type PublicResourceAccessInput = {
|
||||
mode: string;
|
||||
fullDomain: string | null;
|
||||
ssl: boolean;
|
||||
proxyPort: number | null;
|
||||
wildcard: boolean;
|
||||
exitNodeEndpoint?: string | null;
|
||||
};
|
||||
|
||||
export type SiteResourceAccessInput = {
|
||||
mode: string;
|
||||
destination: string | null;
|
||||
destinationPort: number | null;
|
||||
scheme: "http" | "https" | null;
|
||||
ssl: boolean;
|
||||
fullDomain: string | null;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
};
|
||||
|
||||
export type LauncherAccessFields = {
|
||||
accessDisplay: string;
|
||||
accessCopyValue: string;
|
||||
accessUrl: string | null;
|
||||
};
|
||||
|
||||
function formatTcpUdpResourceAccess(
|
||||
exitNodeEndpoint: string | null | undefined,
|
||||
proxyPort: number | null
|
||||
): LauncherAccessFields {
|
||||
if (proxyPort == null) {
|
||||
return {
|
||||
accessDisplay: "",
|
||||
accessCopyValue: "",
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
if (!exitNodeEndpoint?.trim()) {
|
||||
const port = proxyPort.toString();
|
||||
return {
|
||||
accessDisplay: port,
|
||||
accessCopyValue: port,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
const parsed = parseEndpoint(exitNodeEndpoint);
|
||||
const host = parsed?.ip ?? exitNodeEndpoint.trim();
|
||||
const access = formatEndpoint(host, proxyPort);
|
||||
|
||||
return {
|
||||
accessDisplay: access,
|
||||
accessCopyValue: access,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
export function formatPublicResourceAccess(
|
||||
resource: PublicResourceAccessInput
|
||||
): LauncherAccessFields {
|
||||
const browserModes = ["http", "ssh", "rdp", "vnc"];
|
||||
if (!browserModes.includes(resource.mode)) {
|
||||
return formatTcpUdpResourceAccess(
|
||||
resource.exitNodeEndpoint,
|
||||
resource.proxyPort
|
||||
);
|
||||
}
|
||||
|
||||
if (!resource.fullDomain) {
|
||||
return {
|
||||
accessDisplay: "",
|
||||
accessCopyValue: "",
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||
return {
|
||||
accessDisplay: url,
|
||||
accessCopyValue: url,
|
||||
accessUrl: resource.wildcard ? null : url
|
||||
};
|
||||
}
|
||||
|
||||
export function formatSiteResourceAccess(
|
||||
resource: SiteResourceAccessInput
|
||||
): LauncherAccessFields {
|
||||
if (resource.alias) {
|
||||
return {
|
||||
accessDisplay: resource.alias,
|
||||
accessCopyValue: resource.alias,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
if (resource.mode === "http" && resource.fullDomain) {
|
||||
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
|
||||
return {
|
||||
accessDisplay: url,
|
||||
accessCopyValue: url,
|
||||
accessUrl: url
|
||||
};
|
||||
}
|
||||
|
||||
const destination = formatSiteResourceDestinationDisplay({
|
||||
mode: resource.mode as SiteResourceDestinationInput["mode"],
|
||||
destination: resource.destination,
|
||||
destinationPort: resource.destinationPort,
|
||||
scheme: resource.scheme
|
||||
});
|
||||
|
||||
if (destination) {
|
||||
return {
|
||||
accessDisplay: destination,
|
||||
accessCopyValue: destination,
|
||||
accessUrl: resource.mode === "http" ? destination : null
|
||||
};
|
||||
}
|
||||
|
||||
if (resource.aliasAddress) {
|
||||
return {
|
||||
accessDisplay: resource.aliasAddress,
|
||||
accessCopyValue: resource.aliasAddress,
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
accessDisplay: "",
|
||||
accessCopyValue: "",
|
||||
accessUrl: null
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from "./types";
|
||||
export { listLauncherGroups } from "./listLauncherGroups";
|
||||
export { listLauncherResources } from "./listLauncherResources";
|
||||
export { listLauncherSites } from "./listLauncherSites";
|
||||
export { listLauncherLabels } from "./listLauncherLabels";
|
||||
export { listLauncherViews } from "./listLauncherViews";
|
||||
export { createLauncherView } from "./createLauncherView";
|
||||
export { updateLauncherView } from "./updateLauncherView";
|
||||
export { deleteLauncherView } from "./deleteLauncherView";
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { listLauncherGroupsForUser } from "./launcherResourceAccess";
|
||||
import { launcherListQuerySchema } from "./types";
|
||||
|
||||
export async function listLauncherGroups(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = launcherListQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { groups, total } = await listLauncherGroupsForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
groups,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher groups retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher groups:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { listAccessibleLauncherLabelsForUser } from "./launcherResourceAccess";
|
||||
import { launcherFilterListQuerySchema } from "./types";
|
||||
|
||||
export async function listLauncherLabels(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = launcherFilterListQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { labels, total } = await listAccessibleLauncherLabelsForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
labels,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher labels retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher labels:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { listLauncherResourcesForUser } from "./launcherResourceAccess";
|
||||
import { launcherListQuerySchema } from "./types";
|
||||
|
||||
const listLauncherResourcesQuerySchema = launcherListQuerySchema.extend({
|
||||
groupKey: z.string().min(1)
|
||||
});
|
||||
|
||||
export async function listLauncherResources(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = listLauncherResourcesQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resources, total } = await listLauncherResourcesForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
resources,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher resources retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher resources:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { listAccessibleLauncherSitesForUser } from "./launcherResourceAccess";
|
||||
import { launcherFilterListQuerySchema } from "./types";
|
||||
|
||||
export async function listLauncherSites(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = launcherFilterListQuerySchema.safeParse(req.query);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { sites, total } = await listAccessibleLauncherSitesForUser(
|
||||
orgId,
|
||||
userId,
|
||||
req.userOrgRoleIds ?? [],
|
||||
parsed.data
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
sites,
|
||||
pagination: {
|
||||
total,
|
||||
page: parsed.data.page,
|
||||
pageSize: parsed.data.pageSize
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher sites retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher sites:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { launcherViewConfigSchema, type LauncherViewRecord } from "./types";
|
||||
|
||||
function mapViewRow(
|
||||
row: typeof launcherViews.$inferSelect
|
||||
): LauncherViewRecord {
|
||||
return {
|
||||
viewId: row.viewId,
|
||||
orgId: row.orgId,
|
||||
userId: row.userId,
|
||||
name: row.name,
|
||||
config: launcherViewConfigSchema.parse(JSON.parse(row.config)),
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
isOrgWide: row.userId == null
|
||||
};
|
||||
}
|
||||
|
||||
export async function listLauncherViews(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
if (!orgId) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
||||
);
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.orgId, orgId),
|
||||
or(
|
||||
eq(launcherViews.userId, userId),
|
||||
isNull(launcherViews.userId)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
views: rows.map(mapViewRow)
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher views retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error listing launcher views:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const LAUNCHER_UNLABELED_GROUP_KEY = "unlabeled";
|
||||
export const LAUNCHER_NO_SITE_GROUP_KEY = "no-site";
|
||||
|
||||
export const launcherViewConfigSchema = z.object({
|
||||
groupBy: z.enum(["site", "label"]).default("site"),
|
||||
layout: z.enum(["grid", "list"]).default("grid"),
|
||||
sortBy: z.literal("name").default("name"),
|
||||
order: z.enum(["asc", "desc"]).default("asc"),
|
||||
showLabels: z.boolean().default(true),
|
||||
showSiteTags: z.boolean().default(true),
|
||||
showRecents: z.boolean().default(false).optional(),
|
||||
siteIds: z.array(z.number()).default([]),
|
||||
labelIds: z.array(z.number()).default([]),
|
||||
query: z.string().default("")
|
||||
});
|
||||
|
||||
export type LauncherViewConfig = z.infer<typeof launcherViewConfigSchema>;
|
||||
|
||||
export const defaultLauncherViewConfig: LauncherViewConfig =
|
||||
launcherViewConfigSchema.parse({});
|
||||
|
||||
export type LauncherLabel = {
|
||||
labelId: number;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
export type LauncherSiteInfo = {
|
||||
siteId: number;
|
||||
name: string;
|
||||
type: string;
|
||||
online?: boolean;
|
||||
};
|
||||
|
||||
export type LauncherResource = {
|
||||
launcherResourceKey: string;
|
||||
resourceType: "public" | "site";
|
||||
resourceId: number;
|
||||
siteResourceId?: number;
|
||||
niceId: string;
|
||||
name: string;
|
||||
accessDisplay: string;
|
||||
accessCopyValue: string;
|
||||
accessUrl: string | null;
|
||||
iconUrl: string | null;
|
||||
enabled: boolean;
|
||||
mode: string;
|
||||
labels: LauncherLabel[];
|
||||
site?: LauncherSiteInfo;
|
||||
};
|
||||
|
||||
export type LauncherGroup = {
|
||||
groupKey: string;
|
||||
name: string;
|
||||
groupType: "site" | "label";
|
||||
itemCount: number;
|
||||
siteType?: string;
|
||||
siteOnline?: boolean;
|
||||
labelColor?: string;
|
||||
};
|
||||
|
||||
export type ListLauncherGroupsResponse = {
|
||||
groups: LauncherGroup[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListLauncherResourcesResponse = {
|
||||
resources: LauncherResource[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type LauncherViewRecord = {
|
||||
viewId: number;
|
||||
orgId: string;
|
||||
userId: string | null;
|
||||
name: string;
|
||||
config: LauncherViewConfig;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isOrgWide: boolean;
|
||||
};
|
||||
|
||||
export type ListLauncherViewsResponse = {
|
||||
views: LauncherViewRecord[];
|
||||
};
|
||||
|
||||
export const launcherFilterListQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(500)
|
||||
.default(500),
|
||||
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
|
||||
query: z.string().optional().default("")
|
||||
});
|
||||
|
||||
export type LauncherFilterListQuery = z.infer<
|
||||
typeof launcherFilterListQuerySchema
|
||||
>;
|
||||
|
||||
export type ListLauncherSitesResponse = {
|
||||
sites: LauncherSiteInfo[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type ListLauncherLabelsResponse = {
|
||||
labels: LauncherLabel[];
|
||||
pagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const launcherListQuerySchema = z.strictObject({
|
||||
pageSize: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(20)
|
||||
.default(20),
|
||||
page: z.coerce.number().int().min(1).optional().catch(1).default(1),
|
||||
query: z.string().optional().default(""),
|
||||
groupBy: z.enum(["site", "label"]).optional().default("site"),
|
||||
groupKey: z.string().optional(),
|
||||
siteIds: z.string().optional(),
|
||||
labelIds: z.string().optional(),
|
||||
sort_by: z.literal("name").optional().default("name"),
|
||||
order: z.enum(["asc", "desc"]).optional().default("asc")
|
||||
});
|
||||
|
||||
export type LauncherListQuery = z.infer<typeof launcherListQuerySchema>;
|
||||
|
||||
export function parseIdListParam(value: string | undefined): number[] {
|
||||
if (!value?.trim()) {
|
||||
return [];
|
||||
}
|
||||
return value
|
||||
.split(",")
|
||||
.map((part) => Number.parseInt(part.trim(), 10))
|
||||
.filter((id) => Number.isFinite(id));
|
||||
}
|
||||
|
||||
export const DEFAULT_LAUNCHER_VIEW_ID = "default" as const;
|
||||
|
||||
export type LauncherViewSelection =
|
||||
| { type: "default" }
|
||||
| { type: "saved"; viewId: number };
|
||||
@@ -0,0 +1,157 @@
|
||||
import { db, launcherViews } from "@server/db";
|
||||
import { response } from "@server/lib/response";
|
||||
import { getFirstString } from "@server/lib/requestParams";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import moment from "moment";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import { z } from "zod";
|
||||
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
|
||||
import { launcherViewConfigSchema } from "./types";
|
||||
|
||||
const updateLauncherViewBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(128).optional(),
|
||||
config: launcherViewConfigSchema.optional(),
|
||||
orgWide: z.boolean().optional()
|
||||
});
|
||||
|
||||
export async function updateLauncherView(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const orgId = req.userOrgId;
|
||||
const userId = req.user!.userId;
|
||||
const viewId = Number.parseInt(
|
||||
getFirstString(req.params.viewId) ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
if (!orgId || !Number.isFinite(viewId)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Invalid request parameters"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsed = updateLauncherViewBodySchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromZodError(parsed.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(launcherViews)
|
||||
.where(
|
||||
and(
|
||||
eq(launcherViews.viewId, viewId),
|
||||
eq(launcherViews.orgId, orgId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Launcher view not found")
|
||||
);
|
||||
}
|
||||
|
||||
const isPersonalView = existing.userId === userId;
|
||||
const isOrgWideView = existing.userId == null;
|
||||
const canManageOrgWide = await checkUserActionPermission(
|
||||
ActionsEnum.createOrgWideLauncherView,
|
||||
req
|
||||
);
|
||||
|
||||
if (!isPersonalView && !(isOrgWideView && canManageOrgWide)) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"You do not have permission to update this view"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.data.orgWide === true && !canManageOrgWide) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
parsed.data.orgWide === false &&
|
||||
isOrgWideView &&
|
||||
!canManageOrgWide
|
||||
) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have permission perform this action"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const nextUserId =
|
||||
parsed.data.orgWide === true
|
||||
? null
|
||||
: parsed.data.orgWide === false
|
||||
? userId
|
||||
: existing.userId;
|
||||
|
||||
const [updated] = await db
|
||||
.update(launcherViews)
|
||||
.set({
|
||||
name: parsed.data.name ?? existing.name,
|
||||
config: parsed.data.config
|
||||
? JSON.stringify(parsed.data.config)
|
||||
: existing.config,
|
||||
userId: nextUserId,
|
||||
updatedAt: moment().toISOString()
|
||||
})
|
||||
.where(eq(launcherViews.viewId, viewId))
|
||||
.returning();
|
||||
|
||||
return response(res, {
|
||||
data: {
|
||||
viewId: updated.viewId,
|
||||
orgId: updated.orgId,
|
||||
userId: updated.userId,
|
||||
name: updated.name,
|
||||
config: launcherViewConfigSchema.parse(
|
||||
JSON.parse(updated.config)
|
||||
),
|
||||
createdAt: updated.createdAt,
|
||||
updatedAt: updated.updatedAt,
|
||||
isOrgWide: updated.userId == null
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Launcher view updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
if (createHttpError.isHttpError(error)) {
|
||||
return next(error);
|
||||
}
|
||||
console.error("Error updating launcher view:", error);
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Internal server error"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
db,
|
||||
ExitNode,
|
||||
networks,
|
||||
remoteExitNodeResources,
|
||||
resources,
|
||||
Site,
|
||||
siteNetworks,
|
||||
@@ -223,7 +224,8 @@ export async function buildClientConfigurationForNewtClient(
|
||||
|
||||
export async function buildTargetConfigurationForNewtClient(
|
||||
siteId: number,
|
||||
version?: string | null
|
||||
version?: string | null,
|
||||
remoteExitNodeId?: string
|
||||
) {
|
||||
// Get all enabled targets with their resource mode information
|
||||
const allTargets = await db
|
||||
@@ -379,10 +381,24 @@ export async function buildTargetConfigurationForNewtClient(
|
||||
};
|
||||
});
|
||||
|
||||
let remoteExitNodeSubnets: string[] = [];
|
||||
if (remoteExitNodeId) {
|
||||
const remoteNodeResources = await db
|
||||
.select()
|
||||
.from(remoteExitNodeResources)
|
||||
.where(
|
||||
eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
|
||||
);
|
||||
|
||||
// filter through these and provide the subnets
|
||||
remoteExitNodeSubnets = remoteNodeResources.map((r) => r.destination);
|
||||
}
|
||||
|
||||
return {
|
||||
validHealthCheckTargets,
|
||||
tcpTargets,
|
||||
udpTargets,
|
||||
browserGatewayTargets
|
||||
browserGatewayTargets,
|
||||
remoteExitNodeSubnets
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,20 @@ import { Newt } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import logger from "@server/logger";
|
||||
import { sendNewtSyncMessage } from "./sync";
|
||||
import { recordPing } from "./pingAccumulator";
|
||||
import semver from "semver";
|
||||
import { recordSitePing } from "./pingAccumulator";
|
||||
|
||||
const NEWT_SUPPORTS_SYNC_VERSION = ">=1.14.0";
|
||||
const PONG = {
|
||||
message: {
|
||||
type: "pong",
|
||||
data: {
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
excludeSender: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles ping messages from newt clients.
|
||||
@@ -35,7 +48,15 @@ export const handleNewtPingMessage: MessageHandler = async (context) => {
|
||||
// batched UPDATE instead of one query per ping. This prevents
|
||||
// connection pool exhaustion under load, especially with
|
||||
// cross-region latency to the database.
|
||||
recordPing(newt.siteId);
|
||||
recordSitePing(newt.siteId);
|
||||
|
||||
if (
|
||||
newt.version &&
|
||||
!semver.satisfies(newt.version, NEWT_SUPPORTS_SYNC_VERSION)
|
||||
) {
|
||||
// Newt does not support the sync message so not checking - stop here -
|
||||
return PONG;
|
||||
}
|
||||
|
||||
// Check config version and sync if stale.
|
||||
const configVersion = await getClientConfigVersion(newt.newtId);
|
||||
@@ -49,32 +70,21 @@ 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 {
|
||||
message: {
|
||||
type: "pong",
|
||||
data: {
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
},
|
||||
broadcast: false,
|
||||
excludeSender: false
|
||||
};
|
||||
return PONG;
|
||||
};
|
||||
|
||||
@@ -38,7 +38,8 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
||||
const exitNodesList = await listExitNodes(
|
||||
site.orgId,
|
||||
true,
|
||||
noCloud || false
|
||||
noCloud || false,
|
||||
newt.siteId
|
||||
); // filter for only the online ones
|
||||
|
||||
let lastExitNodeId = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { db, ExitNode, newts, Transaction } from "@server/db";
|
||||
import { db, ExitNode, newts, remoteExitNodes, Transaction } from "@server/db";
|
||||
import { MessageHandler } from "@server/routers/ws";
|
||||
import { exitNodes, Newt, sites } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
@@ -196,12 +196,29 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
.where(eq(newts.newtId, newt.newtId));
|
||||
}
|
||||
|
||||
let remoteExitNodeId: string | undefined;
|
||||
if (exitNode.type == "remoteExitNode") {
|
||||
// get the remote exit node ID associated with this exit node
|
||||
const [remoteExitNode] = await db
|
||||
.select()
|
||||
.from(remoteExitNodes)
|
||||
.where(eq(remoteExitNodes.exitNodeId, exitNode.exitNodeId))
|
||||
.limit(1);
|
||||
|
||||
remoteExitNodeId = remoteExitNode?.remoteExitNodeId;
|
||||
}
|
||||
|
||||
const {
|
||||
tcpTargets,
|
||||
udpTargets,
|
||||
validHealthCheckTargets,
|
||||
browserGatewayTargets
|
||||
} = await buildTargetConfigurationForNewtClient(siteId, newtVersion);
|
||||
browserGatewayTargets,
|
||||
remoteExitNodeSubnets
|
||||
} = await buildTargetConfigurationForNewtClient(
|
||||
siteId,
|
||||
newtVersion,
|
||||
remoteExitNodeId // this is for the remote node resources
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}`
|
||||
@@ -222,6 +239,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||
},
|
||||
healthCheckTargets: validHealthCheckTargets,
|
||||
browserGatewayTargets: browserGatewayTargets,
|
||||
remoteExitNodeSubnets: remoteExitNodeSubnets,
|
||||
chainId: chainId
|
||||
}
|
||||
},
|
||||
|
||||
@@ -57,9 +57,6 @@ export function recordSitePing(siteId: number): void {
|
||||
pendingSitePings.set(siteId, now);
|
||||
}
|
||||
|
||||
/** @deprecated Use `recordSitePing` instead. Alias kept for existing call-sites. */
|
||||
export const recordPing = recordSitePing;
|
||||
|
||||
/**
|
||||
* Record a ping for an OLM client. Batches the `clients` table update
|
||||
* (`online`, `lastPing`, `archived`) and, when `olmArchived` is true,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -13,9 +13,9 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
||||
tcpTargets,
|
||||
udpTargets,
|
||||
validHealthCheckTargets,
|
||||
browserGatewayTargets
|
||||
browserGatewayTargets,
|
||||
remoteExitNodeSubnets
|
||||
} = await buildTargetConfigurationForNewtClient(site.siteId);
|
||||
|
||||
let exitNode: ExitNode | undefined;
|
||||
if (site.exitNodeId) {
|
||||
[exitNode] = await db
|
||||
@@ -28,7 +28,6 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
||||
site,
|
||||
exitNode
|
||||
);
|
||||
|
||||
await sendToClient(
|
||||
newt.newtId,
|
||||
{
|
||||
@@ -41,7 +40,8 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
|
||||
healthCheckTargets: validHealthCheckTargets,
|
||||
peers: peers,
|
||||
clientTargets: targets,
|
||||
browserGatewayTargets: browserGatewayTargets
|
||||
browserGatewayTargets: browserGatewayTargets,
|
||||
remoteExitNodeSubnets: remoteExitNodeSubnets
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./types";
|
||||
@@ -43,3 +43,37 @@ export type GetRemoteExitNodeResponse = {
|
||||
online: boolean;
|
||||
type: string | null;
|
||||
};
|
||||
|
||||
export type ListRemoteExitNodeResourcesResponse = {
|
||||
resources: {
|
||||
remoteExitNodeResourceId: number;
|
||||
remoteExitNodeId: string;
|
||||
destination: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type SetRemoteExitNodeResourcesResponse = {
|
||||
resources: {
|
||||
remoteExitNodeResourceId: number;
|
||||
remoteExitNodeId: string;
|
||||
destination: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ListRemoteExitNodePreferenceLabelsResponse = {
|
||||
labels: {
|
||||
remoteExitNodePreferenceLabelId: number;
|
||||
labelId: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type SetRemoteExitNodePreferenceLabelsResponse = {
|
||||
labels: {
|
||||
remoteExitNodePreferenceLabelId: number;
|
||||
labelId: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,8 +5,12 @@ import {
|
||||
userSiteResources,
|
||||
roleSiteResources,
|
||||
userOrgRoles,
|
||||
userOrgs
|
||||
userOrgs,
|
||||
labels,
|
||||
siteResourceLabels
|
||||
} from "@server/db";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import { and, eq, inArray, asc, isNotNull, ne, or } from "drizzle-orm";
|
||||
import createHttpError from "http-errors";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
@@ -19,13 +23,33 @@ import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
|
||||
const USER_RESOURCE_ALIASES_CACHE_TTL_SEC = 60;
|
||||
|
||||
const labelFilterQuerySchema = z
|
||||
.preprocess((val) => {
|
||||
if (val === undefined || val === null || val === "") {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
if (typeof val === "string") {
|
||||
return val.split(",");
|
||||
}
|
||||
return undefined;
|
||||
}, z.array(z.string()))
|
||||
.optional()
|
||||
.catch([]);
|
||||
|
||||
function userResourceAliasesCacheKey(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
page: number,
|
||||
pageSize: number
|
||||
pageSize: number,
|
||||
includeLabels: boolean,
|
||||
labelFilter: string[]
|
||||
) {
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}`;
|
||||
const labelsKey =
|
||||
labelFilter.length > 0 ? labelFilter.slice().sort().join(",") : "all";
|
||||
return `userResourceAliases:${orgId}:${userId}:${page}:${pageSize}:${includeLabels ? "labels" : "plain"}:${labelsKey}`;
|
||||
}
|
||||
|
||||
const listUserResourceAliasesParamsSchema = z.strictObject({
|
||||
@@ -56,43 +80,35 @@ const listUserResourceAliasesQuerySchema = z.strictObject({
|
||||
type: "integer",
|
||||
default: 1,
|
||||
description: "Page number to retrieve"
|
||||
})
|
||||
}),
|
||||
includeLabels: z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.default("false")
|
||||
.transform((val) => val === "true")
|
||||
.openapi({
|
||||
type: "boolean",
|
||||
default: false,
|
||||
description:
|
||||
"When true, include label names for each alias in the items field"
|
||||
}),
|
||||
labels: labelFilterQuerySchema.openapi({
|
||||
type: "array",
|
||||
description:
|
||||
"Filter by resource labels. A resource matches when it has any of the given labels (OR)."
|
||||
})
|
||||
});
|
||||
|
||||
export type UserResourceAliasItem = {
|
||||
alias: string;
|
||||
labels: string[];
|
||||
};
|
||||
|
||||
export type ListUserResourceAliasesResponse = PaginatedResponse<{
|
||||
aliases: string[];
|
||||
items?: UserResourceAliasItem[];
|
||||
}>;
|
||||
|
||||
// registry.registerPath({
|
||||
// method: "get",
|
||||
// path: "/org/{orgId}/user-resource-aliases",
|
||||
// description:
|
||||
// "List private (host-mode) site resource aliases the authenticated user can access in the organization, paginated.",
|
||||
// tags: [OpenAPITags.PrivateResource],
|
||||
// request: {
|
||||
// params: z.object({
|
||||
// orgId: z.string()
|
||||
// }),
|
||||
// query: listUserResourceAliasesQuerySchema
|
||||
// },
|
||||
// responses: {
|
||||
// 200: {
|
||||
// description: "Successful response",
|
||||
// content: {
|
||||
// "application/json": {
|
||||
// schema: z.object({
|
||||
// data: z.record(z.string(), z.any()).nullable(),
|
||||
// success: z.boolean(),
|
||||
// error: z.boolean(),
|
||||
// message: z.string(),
|
||||
// status: z.number()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
export async function listUserResourceAliases(
|
||||
req: Request,
|
||||
res: Response,
|
||||
@@ -110,7 +126,12 @@ export async function listUserResourceAliases(
|
||||
)
|
||||
);
|
||||
}
|
||||
const { page, pageSize } = parsedQuery.data;
|
||||
const {
|
||||
page,
|
||||
pageSize,
|
||||
includeLabels,
|
||||
labels: labelFilter
|
||||
} = parsedQuery.data;
|
||||
|
||||
const parsedParams = listUserResourceAliasesParamsSchema.safeParse(
|
||||
req.params
|
||||
@@ -149,7 +170,9 @@ export async function listUserResourceAliases(
|
||||
orgId,
|
||||
userId,
|
||||
page,
|
||||
pageSize
|
||||
pageSize,
|
||||
includeLabels,
|
||||
labelFilter ?? []
|
||||
);
|
||||
const cachedData: ListUserResourceAliasesResponse | undefined =
|
||||
await cache.get(cacheKey);
|
||||
@@ -204,6 +227,7 @@ export async function listUserResourceAliases(
|
||||
if (accessibleSiteResourceIds.length === 0) {
|
||||
const data: ListUserResourceAliasesResponse = {
|
||||
aliases: [],
|
||||
...(includeLabels ? { items: [] } : {}),
|
||||
pagination: {
|
||||
total: 0,
|
||||
pageSize,
|
||||
@@ -224,18 +248,44 @@ export async function listUserResourceAliases(
|
||||
});
|
||||
}
|
||||
|
||||
const whereClause = and(
|
||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
||||
orgId,
|
||||
tierMatrix.labels
|
||||
);
|
||||
|
||||
const whereConditions = [
|
||||
eq(siteResources.orgId, orgId),
|
||||
eq(siteResources.enabled, true),
|
||||
or(eq(siteResources.mode, "host"), eq(siteResources.mode, "ssh")),
|
||||
isNotNull(siteResources.alias),
|
||||
ne(siteResources.alias, ""),
|
||||
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
|
||||
);
|
||||
];
|
||||
|
||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
||||
whereConditions.push(
|
||||
inArray(
|
||||
siteResources.siteResourceId,
|
||||
db
|
||||
.select({ id: siteResourceLabels.siteResourceId })
|
||||
.from(siteResourceLabels)
|
||||
.innerJoin(
|
||||
labels,
|
||||
eq(labels.labelId, siteResourceLabels.labelId)
|
||||
)
|
||||
.where(inArray(labels.name, labelFilter))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const whereClause = and(...whereConditions);
|
||||
|
||||
const baseSelect = () =>
|
||||
db
|
||||
.select({ alias: siteResources.alias })
|
||||
.select({
|
||||
alias: siteResources.alias,
|
||||
siteResourceId: siteResources.siteResourceId
|
||||
})
|
||||
.from(siteResources)
|
||||
.where(whereClause);
|
||||
|
||||
@@ -251,8 +301,46 @@ export async function listUserResourceAliases(
|
||||
|
||||
const aliases = rows.map((r) => r.alias as string);
|
||||
|
||||
let items: UserResourceAliasItem[] | undefined;
|
||||
if (includeLabels) {
|
||||
const siteResourceIdList = rows.map((r) => r.siteResourceId);
|
||||
|
||||
let labelsForSiteResources: Array<{
|
||||
name: string;
|
||||
siteResourceId: number;
|
||||
}> = [];
|
||||
|
||||
if (isLabelFeatureEnabled && siteResourceIdList.length > 0) {
|
||||
labelsForSiteResources = await db
|
||||
.select({
|
||||
name: labels.name,
|
||||
siteResourceId: siteResourceLabels.siteResourceId
|
||||
})
|
||||
.from(labels)
|
||||
.innerJoin(
|
||||
siteResourceLabels,
|
||||
eq(siteResourceLabels.labelId, labels.labelId)
|
||||
)
|
||||
.where(
|
||||
inArray(
|
||||
siteResourceLabels.siteResourceId,
|
||||
siteResourceIdList
|
||||
)
|
||||
)
|
||||
.orderBy(asc(siteResourceLabels.siteResourceLabelId));
|
||||
}
|
||||
|
||||
items = rows.map((row) => ({
|
||||
alias: row.alias as string,
|
||||
labels: labelsForSiteResources
|
||||
.filter((l) => l.siteResourceId === row.siteResourceId)
|
||||
.map((l) => l.name)
|
||||
}));
|
||||
}
|
||||
|
||||
const data: ListUserResourceAliasesResponse = {
|
||||
aliases,
|
||||
...(items !== undefined ? { items } : {}),
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
pageSize,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from "./listSites";
|
||||
export * from "./listSiteRoles";
|
||||
export * from "./pickSiteDefaults";
|
||||
export * from "./socketIntegration";
|
||||
export * from "./restartSite";
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { db, newts } from "@server/db";
|
||||
import { sites } from "@server/db";
|
||||
import { 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 { sendToClient } from "#dynamic/routers/ws";
|
||||
|
||||
const updateSiteParamsSchema = z.strictObject({
|
||||
siteId: z.coerce.number().int().positive()
|
||||
});
|
||||
|
||||
registry.registerPath({
|
||||
method: "post",
|
||||
path: "/site/{siteId}/restart",
|
||||
description: "Restart a site.",
|
||||
tags: [OpenAPITags.Site],
|
||||
request: {
|
||||
params: updateSiteParamsSchema
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successful response",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
data: z.record(z.string(), z.any()).nullable(),
|
||||
success: z.boolean(),
|
||||
error: z.boolean(),
|
||||
message: z.string(),
|
||||
status: z.number()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export async function restartSite(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = updateSiteParamsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId } = parsedParams.data;
|
||||
|
||||
const [existingSite] = await db
|
||||
.select()
|
||||
.from(sites)
|
||||
.where(eq(sites.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!existingSite) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with ID ${siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// get the newt
|
||||
|
||||
const [newt] = await db
|
||||
.select()
|
||||
.from(newts)
|
||||
.where(eq(newts.siteId, siteId))
|
||||
.limit(1);
|
||||
|
||||
if (!newt) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Newt for site with ID ${siteId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Restarting site ${siteId}...`);
|
||||
|
||||
await sendToClient(newt.newtId, {
|
||||
type: "newt/wg/restart",
|
||||
data: {}
|
||||
});
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Site restarted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,10 @@ 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({
|
||||
@@ -108,6 +111,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."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if any of the roleIds are admin roles
|
||||
const rolesToCheck = await db
|
||||
.select()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "@server/db";
|
||||
import { recordPing } from "@server/routers/newt/pingAccumulator";
|
||||
import { recordSitePing } from "@server/routers/newt/pingAccumulator";
|
||||
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
|
||||
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
|
||||
import { messageHandlers } from "./messageHandlers";
|
||||
@@ -424,7 +424,7 @@ const setupConnection = async (
|
||||
// pending pings in a single batched UPDATE every ~10s, which
|
||||
// prevents connection pool exhaustion under load (especially
|
||||
// with cross-region latency to the database).
|
||||
recordPing(newtClient.siteId);
|
||||
recordSitePing(newtClient.siteId);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user