Merge branch 'dev' into feat/geoip-country-is-not-rule

This commit is contained in:
Fred KISSIE
2026-07-03 22:50:56 +02:00
183 changed files with 11173 additions and 1880 deletions
@@ -17,3 +17,4 @@ export * from "./queryAccessAuditLog";
export * from "./exportAccessAuditLog";
export * from "./queryConnectionAuditLog";
export * from "./exportConnectionAuditLog";
export * from "./logAccessAuditAttempt";
@@ -0,0 +1,95 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { NextFunction } from "express";
import { Request, Response } from "express";
import { z } from "zod";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import response from "@server/lib/response";
import logger from "@server/logger";
import { logAccessAudit } from "#private/lib/logAccessAudit";
export const logAccessAuditAttemptSchema = z.object({
resourceId: z.number().int().positive(),
action: z.boolean(),
type: z.enum(["login", "ssh", "vnc", "rdp"])
});
export const logAccessAuditAttemptParams = z.object({
orgId: z.string()
});
export async function logAccessAuditAttempt(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = logAccessAuditAttemptSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error)
)
);
}
const parsedParams = logAccessAuditAttemptParams.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const { orgId } = parsedParams.data;
const { resourceId, action, type } = parsedBody.data;
const username = req.user?.username;
const userId = req.user?.userId;
await logAccessAudit({
orgId: orgId,
resourceId: resourceId,
action: action,
...(username && userId
? {
user: {
username,
userId
}
}
: {}),
type: type,
userAgent: req.headers["user-agent"],
requestIp: req.ip
});
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Access audit attempt logged successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -22,7 +22,7 @@ import {
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
import { eq, gt, lt, and, count, desc, inArray, isNull } from "drizzle-orm";
import { eq, gt, lt, and, count, desc, inArray, isNull, or } from "drizzle-orm";
import { OpenAPITags } from "@server/openApi";
import { z } from "zod";
import createHttpError from "http-errors";
@@ -120,7 +120,10 @@ function getWhere(data: Q) {
lt(accessAuditLog.timestamp, data.timeEnd),
eq(accessAuditLog.orgId, data.orgId),
data.resourceId
? eq(accessAuditLog.resourceId, data.resourceId)
? or(
eq(accessAuditLog.resourceId, data.resourceId),
eq(accessAuditLog.siteResourceId, data.resourceId)
)
: undefined,
data.actor ? eq(accessAuditLog.actor, data.actor) : undefined,
data.actorType
@@ -233,7 +236,6 @@ async function enrichWithResourceDetails(
const details = siteResourceMap.get(log.siteResourceId);
return {
...log,
resourceId: log.siteResourceId,
resourceName: details?.name ?? null,
resourceNiceId: details?.niceId ?? null
};
+2 -2
View File
@@ -25,7 +25,7 @@ import {
getTier1FeaturePriceSet,
getTier3FeaturePriceSet,
getTier2FeaturePriceSet,
FeatureId,
LimitId,
type FeaturePriceSet
} from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems";
@@ -214,7 +214,7 @@ export async function changeTier(
}
// Map to the corresponding feature in the new tier
const newPriceId = targetPriceSet[FeatureId.USERS];
const newPriceId = targetPriceSet[LimitId.USERS];
if (newPriceId) {
return {
+27 -6
View File
@@ -24,7 +24,7 @@ import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { Limit, limits, Usage, usage } from "@server/db";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { LimitId } from "@server/lib/billing";
import { GetOrgUsageResponse } from "@server/routers/billing/types";
const getOrgSchema = z.strictObject({
@@ -93,16 +93,28 @@ export async function getOrgUsage(
// Get usage for org
const usageData = [];
const sites = await usageService.getUsage(orgId, FeatureId.SITES);
const users = await usageService.getUsage(orgId, FeatureId.USERS);
const domains = await usageService.getUsage(orgId, FeatureId.DOMAINS);
const sites = await usageService.getUsage(orgId, LimitId.SITES);
const users = await usageService.getUsage(orgId, LimitId.USERS);
const domains = await usageService.getUsage(orgId, LimitId.DOMAINS);
const remoteExitNodes = await usageService.getUsage(
orgId,
FeatureId.REMOTE_EXIT_NODES
LimitId.REMOTE_EXIT_NODES
);
const organizations = await usageService.getUsage(
orgId,
FeatureId.ORGINIZATIONS
LimitId.ORGANIZATIONS
);
const publicResources = await usageService.getUsage(
orgId,
LimitId.PUBLIC_RESOURCES
);
const privateResources = await usageService.getUsage(
orgId,
LimitId.PRIVATE_RESOURCES
);
const machineClients = await usageService.getUsage(
orgId,
LimitId.MACHINE_CLIENTS
);
// const egressData = await usageService.getUsage(
// orgId,
@@ -127,6 +139,15 @@ export async function getOrgUsage(
if (organizations) {
usageData.push(organizations);
}
if (publicResources) {
usageData.push(publicResources);
}
if (privateResources) {
usageData.push(privateResources);
}
if (machineClients) {
usageData.push(machineClients);
}
const orgLimits = await db
.select()
+66 -20
View File
@@ -329,6 +329,44 @@ authenticated.delete(
remoteExitNode.deleteRemoteExitNode
);
authenticated.get(
"/org/:orgId/remote-exit-node/:remoteExitNodeId/resources",
verifyValidLicense,
verifyOrgAccess,
verifyRemoteExitNodeAccess,
verifyUserHasAction(ActionsEnum.getRemoteExitNode),
remoteExitNode.listRemoteExitNodeResources
);
authenticated.post(
"/org/:orgId/remote-exit-node/:remoteExitNodeId/resources",
verifyValidLicense,
verifyOrgAccess,
verifyRemoteExitNodeAccess,
verifyUserHasAction(ActionsEnum.updateRemoteExitNode),
logActionAudit(ActionsEnum.updateRemoteExitNode),
remoteExitNode.setRemoteExitNodeResources
);
authenticated.get(
"/org/:orgId/remote-exit-node/:remoteExitNodeId/preference-labels",
verifyValidLicense,
verifyOrgAccess,
verifyRemoteExitNodeAccess,
verifyUserHasAction(ActionsEnum.getRemoteExitNode),
remoteExitNode.listRemoteExitNodePreferenceLabels
);
authenticated.post(
"/org/:orgId/remote-exit-node/:remoteExitNodeId/preference-labels",
verifyValidLicense,
verifyOrgAccess,
verifyRemoteExitNodeAccess,
verifyUserHasAction(ActionsEnum.updateRemoteExitNode),
logActionAudit(ActionsEnum.updateRemoteExitNode),
remoteExitNode.setRemoteExitNodePreferenceLabels
);
authenticated.put(
"/org/:orgId/login-page",
verifyValidLicense,
@@ -495,29 +533,31 @@ authRouter.post(
auth.transferSession
);
authenticated.post(
"/license/activate",
verifyUserIsServerAdmin,
license.activateLicense
);
if (build !== "saas") {
authenticated.post(
"/license/activate",
verifyUserIsServerAdmin,
license.activateLicense
);
authenticated.get(
"/license/keys",
verifyUserIsServerAdmin,
license.listLicenseKeys
);
authenticated.get(
"/license/keys",
verifyUserIsServerAdmin,
license.listLicenseKeys
);
authenticated.delete(
"/license/:licenseKey",
verifyUserIsServerAdmin,
license.deleteLicenseKey
);
authenticated.delete(
"/license/:licenseKey",
verifyUserIsServerAdmin,
license.deleteLicenseKey
);
authenticated.post(
"/license/recheck",
verifyUserIsServerAdmin,
license.recheckStatus
);
authenticated.post(
"/license/recheck",
verifyUserIsServerAdmin,
license.recheckStatus
);
}
authenticated.get(
"/org/:orgId/logs/action",
@@ -878,3 +918,9 @@ authenticated.post(
verifyClientAccess,
client.rebuildClientAssociationsCacheRoute
);
authenticated.post(
"/org/:orgId/logs/access/attempt",
verifyOrgAccess,
logs.logAccessAuditAttempt
);
@@ -215,7 +215,7 @@ export async function sendTrialNotification(
if (resetLimits) {
// this will only fire if they have not upgraded yet because when upgrading we delete the trial
await handleSubscriptionLifesycle(orgId, "cancled");
await handleSubscriptionLifesycle(orgId, "canceled");
logger.debug(
`Trial ended for org ${orgId}, limits reset to free tier`
);
@@ -23,6 +23,7 @@ import { and, eq, sql } from "drizzle-orm";
import { removeUserFromOrg } from "@server/lib/userOrg";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { OpenAPITags, registry } from "@server/openApi";
import { isOrgRebuildRateLimited } from "@server/lib/rebuildClientAssociations";
const paramsSchema = z
.object({
@@ -90,6 +91,15 @@ export async function unassociateOrgIdp(
);
}
if (await isOrgRebuildRateLimited(org.orgId)) {
return next(
createHttpError(
HttpCode.TOO_MANY_REQUESTS,
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
)
);
}
const orgUsersFromIdp = await db
.select({
userId: userOrgs.userId,
@@ -35,7 +35,7 @@ import logger from "@server/logger";
import { and, eq, inArray, ne } from "drizzle-orm";
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { LimitId } from "@server/lib/billing";
import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
export const paramsSchema = z.object({
@@ -79,7 +79,10 @@ export async function createRemoteExitNode(
const { remoteExitNodeId, secret } = parsedBody.data;
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
if (
req.user &&
(!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
@@ -87,13 +90,13 @@ export async function createRemoteExitNode(
const usage = await usageService.getUsage(
orgId,
FeatureId.REMOTE_EXIT_NODES
LimitId.REMOTE_EXIT_NODES
);
if (usage) {
const rejectRemoteExitNodes = await usageService.checkLimitSet(
orgId,
FeatureId.REMOTE_EXIT_NODES,
LimitId.REMOTE_EXIT_NODES,
{
...usage,
instantaneousValue: (usage.instantaneousValue || 0) + 1
@@ -264,7 +267,7 @@ export async function createRemoteExitNode(
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add(
orgId,
FeatureId.REMOTE_EXIT_NODES,
LimitId.REMOTE_EXIT_NODES,
1,
trx
);
@@ -22,7 +22,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { usageService } from "@server/lib/billing/usageService";
import { FeatureId } from "@server/lib/billing";
import { LimitId } from "@server/lib/billing";
const paramsSchema = z.strictObject({
orgId: z.string().min(1),
@@ -117,7 +117,7 @@ export async function deleteRemoteExitNode(
if (orgsInBillingDomainThatTheNodeIsStillIn.length === 0) {
await usageService.add(
orgId,
FeatureId.REMOTE_EXIT_NODES,
LimitId.REMOTE_EXIT_NODES,
-1,
trx
);
@@ -23,3 +23,7 @@ export * from "./pickRemoteExitNodeDefaults";
export * from "./quickStartRemoteExitNode";
export * from "./offlineChecker";
export * from "./exitNodeReconnectScheduler";
export * from "./listRemoteExitNodeResources";
export * from "./setRemoteExitNodeResources";
export * from "./listRemoteExitNodePreferenceLabels";
export * from "./setRemoteExitNodePreferenceLabels";
@@ -0,0 +1,102 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { NextFunction, Request, Response } from "express";
import { z } from "zod";
import {
db,
labels,
remoteExitNodePreferenceLabels,
remoteExitNodes
} 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 { ListRemoteExitNodePreferenceLabelsResponse } from "@server/routers/remoteExitNode";
const paramsSchema = z.strictObject({
orgId: z.string().min(1),
remoteExitNodeId: z.string().min(1)
});
export async function listRemoteExitNodePreferenceLabels(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { remoteExitNodeId } = parsedParams.data;
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
.limit(1);
if (!remoteExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Remote exit node with ID ${remoteExitNodeId} not found`
)
);
}
const rows = await db
.select({
remoteExitNodePreferenceLabelId:
remoteExitNodePreferenceLabels.remoteExitNodePreferenceLabelId,
labelId: remoteExitNodePreferenceLabels.labelId,
name: labels.name,
color: labels.color
})
.from(remoteExitNodePreferenceLabels)
.innerJoin(
labels,
eq(labels.labelId, remoteExitNodePreferenceLabels.labelId)
)
.where(
eq(
remoteExitNodePreferenceLabels.remoteExitNodeId,
remoteExitNodeId
)
);
return response<ListRemoteExitNodePreferenceLabelsResponse>(res, {
data: { labels: rows },
success: true,
error: false,
message:
"Remote exit node preference labels retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,83 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { NextFunction, Request, Response } from "express";
import { z } from "zod";
import { db, remoteExitNodeResources, remoteExitNodes } 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 { ListRemoteExitNodeResourcesResponse } from "@server/routers/remoteExitNode/types";
const paramsSchema = z.strictObject({
orgId: z.string().min(1),
remoteExitNodeId: z.string().min(1)
});
export async function listRemoteExitNodeResources(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { remoteExitNodeId } = parsedParams.data;
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
.limit(1);
if (!remoteExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Remote exit node with ID ${remoteExitNodeId} not found`
)
);
}
const resources = await db
.select()
.from(remoteExitNodeResources)
.where(
eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
);
return response<ListRemoteExitNodeResourcesResponse>(res, {
data: { resources },
success: true,
error: false,
message: "Remote exit node resources retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,160 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { NextFunction, Request, Response } from "express";
import { z } from "zod";
import {
db,
labels,
remoteExitNodePreferenceLabels,
remoteExitNodes
} from "@server/db";
import { and, eq, inArray } 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 { SetRemoteExitNodePreferenceLabelsResponse } from "@server/routers/remoteExitNode";
const paramsSchema = z.strictObject({
orgId: z.string().min(1),
remoteExitNodeId: z.string().min(1)
});
const bodySchema = z.strictObject({
labelIds: z.array(z.number().int().positive())
});
export type SetRemoteExitNodePreferenceLabelsBody = z.infer<typeof bodySchema>;
export async function setRemoteExitNodePreferenceLabels(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId, remoteExitNodeId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { labelIds } = parsedBody.data;
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
.limit(1);
if (!remoteExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Remote exit node with ID ${remoteExitNodeId} not found`
)
);
}
// Validate all provided labelIds belong to this org
if (labelIds.length > 0) {
const existingLabels = await db
.select({ labelId: labels.labelId })
.from(labels)
.where(
and(
eq(labels.orgId, orgId),
inArray(labels.labelId, labelIds)
)
);
if (existingLabels.length !== labelIds.length) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"One or more label IDs are invalid or do not belong to this organization"
)
);
}
}
// Replace all preference labels atomically
await db
.delete(remoteExitNodePreferenceLabels)
.where(
eq(
remoteExitNodePreferenceLabels.remoteExitNodeId,
remoteExitNodeId
)
);
if (labelIds.length > 0) {
await db.insert(remoteExitNodePreferenceLabels).values(
labelIds.map((labelId) => ({
remoteExitNodeId,
labelId
}))
);
}
const rows = await db
.select({
remoteExitNodePreferenceLabelId:
remoteExitNodePreferenceLabels.remoteExitNodePreferenceLabelId,
labelId: remoteExitNodePreferenceLabels.labelId,
name: labels.name,
color: labels.color
})
.from(remoteExitNodePreferenceLabels)
.innerJoin(
labels,
eq(labels.labelId, remoteExitNodePreferenceLabels.labelId)
)
.where(
eq(
remoteExitNodePreferenceLabels.remoteExitNodeId,
remoteExitNodeId
)
);
return response<SetRemoteExitNodePreferenceLabelsResponse>(res, {
data: { labels: rows },
success: true,
error: false,
message: "Remote exit node preference labels updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -0,0 +1,153 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { NextFunction, Request, Response } from "express";
import { z } from "zod";
import {
db,
newts,
remoteExitNodeResources,
remoteExitNodes,
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 { sendToClientsBatch } from "#private/routers/ws";
import { canCompress } from "@server/lib/clientVersionChecks";
import { SetRemoteExitNodeResourcesResponse } from "@server/routers/remoteExitNode";
const paramsSchema = z.strictObject({
orgId: z.string().min(1),
remoteExitNodeId: z.string().min(1)
});
const cidrRegex =
/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$|^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))$/;
const bodySchema = z.strictObject({
destinations: z.array(
z.string().regex(cidrRegex, "Must be a valid CIDR range")
)
});
export type SetRemoteExitNodeResourcesBody = z.infer<typeof bodySchema>;
export async function setRemoteExitNodeResources(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { remoteExitNodeId } = parsedParams.data;
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { destinations } = parsedBody.data;
const [remoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId))
.limit(1);
if (!remoteExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Remote exit node with ID ${remoteExitNodeId} not found`
)
);
}
// Replace all resources atomically
await db
.delete(remoteExitNodeResources)
.where(
eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
);
if (destinations.length > 0) {
await db.insert(remoteExitNodeResources).values(
destinations.map((destination) => ({
remoteExitNodeId,
destination
}))
);
}
const resources = await db
.select()
.from(remoteExitNodeResources)
.where(
eq(remoteExitNodeResources.remoteExitNodeId, remoteExitNodeId)
);
// Notify all newts connected to this remote exit node's exit node
if (remoteExitNode.exitNodeId) {
const connectedNewts = await db
.select({ newtId: newts.newtId, version: newts.version })
.from(newts)
.innerJoin(sites, eq(newts.siteId, sites.siteId))
.where(eq(sites.exitNodeId, remoteExitNode.exitNodeId));
await sendToClientsBatch(
connectedNewts.map(({ newtId, version }) => ({
clientId: newtId,
message: {
type: "newt/wg/subnets/update",
data: { subnets: destinations }
},
options: {
incrementConfigVersion: true,
compress: canCompress(version, "newt")
}
}))
);
}
return response<SetRemoteExitNodeResourcesResponse>(res, {
data: { resources },
success: true,
error: false,
message: "Remote exit node resources updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
+13 -1
View File
@@ -23,7 +23,10 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import {
rebuildClientAssociationsFromClient,
isOrgRebuildRateLimited
} from "@server/lib/rebuildClientAssociations";
const addUserRoleParamsSchema = z.strictObject({
userId: z.string(),
@@ -128,6 +131,15 @@ export async function addUserRole(
);
}
if (await isOrgRebuildRateLimited(role.orgId)) {
return next(
createHttpError(
HttpCode.TOO_MANY_REQUESTS,
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
)
);
}
let newUserRole: {
userId: string;
orgId: string;
+13 -1
View File
@@ -21,7 +21,10 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import {
rebuildClientAssociationsFromClient,
isOrgRebuildRateLimited
} from "@server/lib/rebuildClientAssociations";
const setUserOrgRolesParamsSchema = z.strictObject({
orgId: z.string(),
@@ -87,6 +90,15 @@ export async function setUserOrgRoles(
);
}
if (await isOrgRebuildRateLimited(orgId)) {
return next(
createHttpError(
HttpCode.TOO_MANY_REQUESTS,
"Too many concurrent rebuild operations for this organization. Please retry after a moment."
)
);
}
const orgRoles = await db
.select({ roleId: roles.roleId, isAdmin: roles.isAdmin })
.from(roles)
+2 -2
View File
@@ -26,7 +26,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 logger from "@server/logger";
@@ -1063,7 +1063,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);
});
}