Merge branch 'rdp-ssh' into dev

This commit is contained in:
Owen
2026-05-28 13:59:14 -07:00
104 changed files with 8491 additions and 3528 deletions
+230 -226
View File
@@ -174,6 +174,7 @@ export async function createSite(
}
let updatedAddress = null;
let releaseSubnetLock: (() => Promise<void>) | null = null;
if (address) {
if (!org.subnet) {
return next(
@@ -244,147 +245,22 @@ export async function createSite(
);
}
} else {
const newClientAddress = await getNextAvailableClientSubnet(orgId);
if (!newClientAddress) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"No available address found"
)
);
}
const { value: newClientAddress, release } =
await getNextAvailableClientSubnet(orgId);
releaseSubnetLock = release;
updatedAddress = newClientAddress.split("/")[0];
}
if (subnet && exitNodeId) {
//make sure the subnet is in the range of the exit node if provided
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId));
if (!exitNode) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
);
}
if (!exitNode.address) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node has no subnet defined"
)
);
}
const subnetIp = subnet.split("/")[0];
if (!isIpInCidr(subnetIp, exitNode.address)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is not in the CIDR range of the exit node address."
)
);
}
// lets also make sure there is no overlap with other sites on the exit node
const sitesQuery = await db
.select({
subnet: sites.subnet
})
.from(sites)
.where(
and(
eq(sites.exitNodeId, exitNodeId),
eq(sites.subnet, subnet)
)
);
if (sitesQuery.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} overlaps with an existing site on this exit node. Please restart site creation.`
)
);
}
}
let updatedNiceId = niceId;
if (!niceId) {
updatedNiceId = await getUniqueSiteName(orgId);
} else {
// make sure the niceId is unique
const existingSite = await db
.select()
.from(sites)
.where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId)))
.limit(1);
if (existingSite.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Nice ID ${niceId} already exists. Please choose a different one.`
)
);
}
}
let newSite: Site | undefined;
await db.transaction(async (trx) => {
if (type == "newt") {
[newSite] = await trx
.insert(sites)
.values({
// NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT
orgId,
name,
niceId: updatedNiceId!,
address: updatedAddress || null,
type,
dockerSocketEnabled: true,
status: "approved"
})
.returning();
await logsDb.insert(statusHistory).values({
entityType: "site",
entityId: newSite.siteId,
orgId: orgId,
status: "offline",
timestamp: Math.floor(Date.now() / 1000)
});
} else if (type == "wireguard") {
// we are creating a site with an exit node (tunneled)
if (!subnet) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is required for tunneled sites"
)
);
}
if (!exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node ID is required for tunneled sites"
)
);
}
const { exitNode, hasAccess } = await verifyExitNodeOrgAccess(
exitNodeId,
orgId
);
try {
if (subnet && exitNodeId) {
//make sure the subnet is in the range of the exit node if provided
const [exitNode] = await db
.select()
.from(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodeId));
if (!exitNode) {
logger.warn("Exit node not found");
return next(
createHttpError(
HttpCode.NOT_FOUND,
@@ -393,118 +269,246 @@ export async function createSite(
);
}
if (!hasAccess) {
logger.warn("Not authorized to use this exit node");
if (!exitNode.address) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Not authorized to use this exit node"
HttpCode.BAD_REQUEST,
"Exit node has no subnet defined"
)
);
}
[newSite] = await trx
.insert(sites)
.values({
orgId,
exitNodeId,
name,
niceId: updatedNiceId!,
subnet,
type,
pubKey: pubKey || null,
status: "approved"
const subnetIp = subnet.split("/")[0];
if (!isIpInCidr(subnetIp, exitNode.address)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is not in the CIDR range of the exit node address."
)
);
}
// lets also make sure there is no overlap with other sites on the exit node
const sitesQuery = await db
.select({
subnet: sites.subnet
})
.returning();
} else if (type == "local") {
[newSite] = await trx
.insert(sites)
.values({
exitNodeId: exitNodeId || null,
orgId,
name,
niceId: updatedNiceId!,
type,
dockerSocketEnabled: false,
online: true,
subnet: "0.0.0.0/32",
status: "approved"
})
.returning();
.from(sites)
.where(
and(
eq(sites.exitNodeId, exitNodeId),
eq(sites.subnet, subnet)
)
);
if (sitesQuery.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} overlaps with an existing site on this exit node. Please restart site creation.`
)
);
}
}
let updatedNiceId = niceId;
if (!niceId) {
updatedNiceId = await getUniqueSiteName(orgId);
} else {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Site type not recognized"
// make sure the niceId is unique
const existingSite = await db
.select()
.from(sites)
.where(
and(eq(sites.niceId, niceId), eq(sites.orgId, orgId))
)
);
.limit(1);
if (existingSite.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Nice ID ${niceId} already exists. Please choose a different one.`
)
);
}
}
const adminRole = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
await db.transaction(async (trx) => {
if (type == "newt") {
[newSite] = await trx
.insert(sites)
.values({
// NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT
orgId,
name,
niceId: updatedNiceId!,
address: updatedAddress || null,
type,
dockerSocketEnabled: true,
status: "approved"
})
.returning();
if (adminRole.length === 0) {
return next(
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
);
}
await logsDb.insert(statusHistory).values({
entityType: "site",
entityId: newSite.siteId,
orgId: orgId,
status: "offline",
timestamp: Math.floor(Date.now() / 1000)
});
} else if (type == "wireguard") {
// we are creating a site with an exit node (tunneled)
if (!subnet) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Subnet is required for tunneled sites"
)
);
}
await trx.insert(roleSites).values({
roleId: adminRole[0].roleId,
siteId: newSite.siteId
});
if (!exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node ID is required for tunneled sites"
)
);
}
if (
req.user &&
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
) {
// make sure the user can access the site
trx.insert(userSites).values({
userId: req.user?.userId!,
const { exitNode, hasAccess } =
await verifyExitNodeOrgAccess(exitNodeId, orgId);
if (!exitNode) {
logger.warn("Exit node not found");
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Exit node not found"
)
);
}
if (!hasAccess) {
logger.warn("Not authorized to use this exit node");
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Not authorized to use this exit node"
)
);
}
[newSite] = await trx
.insert(sites)
.values({
orgId,
exitNodeId,
name,
niceId: updatedNiceId!,
subnet,
type,
pubKey: pubKey || null,
status: "approved"
})
.returning();
} else if (type == "local") {
[newSite] = await trx
.insert(sites)
.values({
exitNodeId: exitNodeId || null,
orgId,
name,
niceId: updatedNiceId!,
type,
dockerSocketEnabled: false,
online: true,
subnet: "0.0.0.0/32",
status: "approved"
})
.returning();
} else {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Site type not recognized"
)
);
}
const adminRole = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (adminRole.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Admin role not found`
)
);
}
await trx.insert(roleSites).values({
roleId: adminRole[0].roleId,
siteId: newSite.siteId
});
}
// add the peer to the exit node
if (type == "newt") {
const secretHash = await hashPassword(updatedNewtSecret);
await trx.insert(newts).values({
newtId: updatedNewtId,
secretHash,
siteId: newSite.siteId,
dateCreated: moment().toISOString()
});
} else if (type == "wireguard") {
if (!pubKey) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Public key is required for wireguard sites"
)
);
if (
req.user &&
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
) {
// make sure the user can access the site
trx.insert(userSites).values({
userId: req.user?.userId!,
siteId: newSite.siteId
});
}
if (!exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node ID is required for wireguard sites"
)
);
// add the peer to the exit node
if (type == "newt") {
const secretHash = await hashPassword(updatedNewtSecret);
await trx.insert(newts).values({
newtId: updatedNewtId,
secretHash,
siteId: newSite.siteId,
dateCreated: moment().toISOString()
});
} else if (type == "wireguard") {
if (!pubKey) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Public key is required for wireguard sites"
)
);
}
if (!exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Exit node ID is required for wireguard sites"
)
);
}
await addPeer(exitNodeId, {
publicKey: pubKey,
allowedIps: []
});
}
await addPeer(exitNodeId, {
publicKey: pubKey,
allowedIps: []
});
}
await usageService.add(orgId, FeatureId.SITES, 1, trx);
});
await usageService.add(orgId, FeatureId.SITES, 1, trx);
});
} finally {
await releaseSubnetLock?.();
}
if (!newSite) {
return next(
+3 -1
View File
@@ -119,7 +119,9 @@ export async function pickSiteDefaults(
);
}
const newClientAddress = await getNextAvailableClientSubnet(orgId);
const { value: newClientAddress, release: releaseSubnetLock } =
await getNextAvailableClientSubnet(orgId);
await releaseSubnetLock(); // release immediately — this endpoint only previews the next available value
if (!newClientAddress) {
return next(
createHttpError(