mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-21 04:56:40 +02:00
Properly lock the ip selection through writes to db
This commit is contained in:
@@ -364,8 +364,14 @@ export async function updateClientResources(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
|
let releaseAliasLock: (() => Promise<void>) | null = null;
|
||||||
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
if (resourceData.mode === "host" || resourceData.mode === "http") {
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId, trx);
|
const { value, release } = await getNextAvailableAliasAddress(
|
||||||
|
orgId,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
aliasAddress = value;
|
||||||
|
releaseAliasLock = release;
|
||||||
}
|
}
|
||||||
|
|
||||||
let domainInfo:
|
let domainInfo:
|
||||||
@@ -427,6 +433,8 @@ export async function updateClientResources(
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
await releaseAliasLock?.();
|
||||||
|
|
||||||
const siteResourceId = newResource.siteResourceId;
|
const siteResourceId = newResource.siteResourceId;
|
||||||
|
|
||||||
for (const site of allSites) {
|
for (const site of allSites) {
|
||||||
|
|||||||
@@ -331,16 +331,8 @@ export async function calculateUserClientsForOrgs(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Get next available subnet
|
// Get next available subnet
|
||||||
const newSubnet = await getNextAvailableClientSubnet(
|
const { value: newSubnet, release: releaseSubnetLock } =
|
||||||
orgId,
|
await getNextAvailableClientSubnet(orgId, transaction);
|
||||||
transaction
|
|
||||||
);
|
|
||||||
if (!newSubnet) {
|
|
||||||
logger.warn(
|
|
||||||
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no available subnet found`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subnet = newSubnet.split("/")[0];
|
const subnet = newSubnet.split("/")[0];
|
||||||
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
|
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`;
|
||||||
@@ -370,6 +362,7 @@ export async function calculateUserClientsForOrgs(
|
|||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values(newClientData)
|
.values(newClientData)
|
||||||
.returning();
|
.returning();
|
||||||
|
await releaseSubnetLock();
|
||||||
existingClientCache.set(
|
existingClientCache.set(
|
||||||
getOrgOlmKey(orgId, olm.olmId),
|
getOrgOlmKey(orgId, olm.olmId),
|
||||||
newClient
|
newClient
|
||||||
|
|||||||
+131
-110
@@ -327,127 +327,145 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
|
|||||||
export async function getNextAvailableClientSubnet(
|
export async function getNextAvailableClientSubnet(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
transaction: Transaction | typeof db = db
|
transaction: Transaction | typeof db = db
|
||||||
): Promise<string> {
|
): Promise<{ value: string; release: () => Promise<void> }> {
|
||||||
return await lockManager.withLock(
|
const lockKey = `client-subnet-allocation:${orgId}`;
|
||||||
`client-subnet-allocation:${orgId}`,
|
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
||||||
async () => {
|
if (!acquired) {
|
||||||
const [org] = await transaction
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
.select()
|
}
|
||||||
.from(orgs)
|
const release = () => lockManager.releaseLock(lockKey);
|
||||||
.where(eq(orgs.orgId, orgId));
|
|
||||||
|
|
||||||
if (!org) {
|
try {
|
||||||
throw new Error(`Organization with ID ${orgId} not found`);
|
const [org] = await transaction
|
||||||
}
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
if (!org.subnet) {
|
if (!org) {
|
||||||
throw new Error(
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
`Organization with ID ${orgId} has no subnet defined`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingAddressesSites = await transaction
|
|
||||||
.select({
|
|
||||||
address: sites.address
|
|
||||||
})
|
|
||||||
.from(sites)
|
|
||||||
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
|
||||||
|
|
||||||
const existingAddressesClients = await transaction
|
|
||||||
.select({
|
|
||||||
address: clients.subnet
|
|
||||||
})
|
|
||||||
.from(clients)
|
|
||||||
.where(
|
|
||||||
and(isNotNull(clients.subnet), eq(clients.orgId, orgId))
|
|
||||||
);
|
|
||||||
|
|
||||||
const addresses = [
|
|
||||||
...existingAddressesSites.map(
|
|
||||||
(site) => `${site.address?.split("/")[0]}/32`
|
|
||||||
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
|
||||||
...existingAddressesClients.map(
|
|
||||||
(client) => `${client.address.split("/")}/32`
|
|
||||||
)
|
|
||||||
].filter((address) => address !== null) as string[];
|
|
||||||
|
|
||||||
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
|
||||||
if (!subnet) {
|
|
||||||
throw new Error("No available subnets remaining in space");
|
|
||||||
}
|
|
||||||
|
|
||||||
return subnet;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
if (!org.subnet) {
|
||||||
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no subnet defined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAddressesSites = await transaction
|
||||||
|
.select({
|
||||||
|
address: sites.address
|
||||||
|
})
|
||||||
|
.from(sites)
|
||||||
|
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
|
||||||
|
|
||||||
|
const existingAddressesClients = await transaction
|
||||||
|
.select({
|
||||||
|
address: clients.subnet
|
||||||
|
})
|
||||||
|
.from(clients)
|
||||||
|
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
|
||||||
|
|
||||||
|
const addresses = [
|
||||||
|
...existingAddressesSites.map(
|
||||||
|
(site) => `${site.address?.split("/")[0]}/32`
|
||||||
|
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
|
||||||
|
...existingAddressesClients.map(
|
||||||
|
(client) => `${client.address.split("/")[0]}/32`
|
||||||
|
)
|
||||||
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
|
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||||
|
if (!subnet) {
|
||||||
|
throw new Error("No available subnets remaining in space");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: subnet, release };
|
||||||
|
} catch (e) {
|
||||||
|
await release();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableAliasAddress(
|
export async function getNextAvailableAliasAddress(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
trx: Transaction | typeof db = db
|
trx: Transaction | typeof db = db
|
||||||
): Promise<string> {
|
): Promise<{ value: string; release: () => Promise<void> }> {
|
||||||
return await lockManager.withLock(
|
const lockKey = `alias-address-allocation:${orgId}`;
|
||||||
`alias-address-allocation:${orgId}`,
|
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
||||||
async () => {
|
if (!acquired) {
|
||||||
const [org] = await trx
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
.select()
|
}
|
||||||
.from(orgs)
|
const release = () => lockManager.releaseLock(lockKey);
|
||||||
.where(eq(orgs.orgId, orgId));
|
|
||||||
|
|
||||||
if (!org) {
|
try {
|
||||||
throw new Error(`Organization with ID ${orgId} not found`);
|
const [org] = await trx
|
||||||
}
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
if (!org.subnet) {
|
if (!org) {
|
||||||
throw new Error(
|
throw new Error(`Organization with ID ${orgId} not found`);
|
||||||
`Organization with ID ${orgId} has no subnet defined`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!org.utilitySubnet) {
|
|
||||||
throw new Error(
|
|
||||||
`Organization with ID ${orgId} has no utility subnet defined`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingAddresses = await trx
|
|
||||||
.select({
|
|
||||||
aliasAddress: siteResources.aliasAddress
|
|
||||||
})
|
|
||||||
.from(siteResources)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
isNotNull(siteResources.aliasAddress),
|
|
||||||
eq(siteResources.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const addresses = [
|
|
||||||
...existingAddresses.map(
|
|
||||||
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
|
||||||
),
|
|
||||||
// reserve a /29 for the dns server and other stuff
|
|
||||||
`${org.utilitySubnet.split("/")[0]}/29`
|
|
||||||
].filter((address) => address !== null) as string[];
|
|
||||||
|
|
||||||
let subnet = findNextAvailableCidr(
|
|
||||||
addresses,
|
|
||||||
32,
|
|
||||||
org.utilitySubnet
|
|
||||||
);
|
|
||||||
if (!subnet) {
|
|
||||||
throw new Error("No available subnets remaining in space");
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the cidr
|
|
||||||
subnet = subnet.split("/")[0];
|
|
||||||
|
|
||||||
return subnet;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
if (!org.subnet) {
|
||||||
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no subnet defined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!org.utilitySubnet) {
|
||||||
|
throw new Error(
|
||||||
|
`Organization with ID ${orgId} has no utility subnet defined`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingAddresses = await trx
|
||||||
|
.select({
|
||||||
|
aliasAddress: siteResources.aliasAddress
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
isNotNull(siteResources.aliasAddress),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const addresses = [
|
||||||
|
...existingAddresses.map(
|
||||||
|
(site) => `${site.aliasAddress?.split("/")[0]}/32`
|
||||||
|
),
|
||||||
|
// reserve a /29 for the dns server and other stuff
|
||||||
|
`${org.utilitySubnet.split("/")[0]}/29`
|
||||||
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
|
let subnet = findNextAvailableCidr(addresses, 32, org.utilitySubnet);
|
||||||
|
if (!subnet) {
|
||||||
|
throw new Error("No available subnets remaining in space");
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the cidr
|
||||||
|
subnet = subnet.split("/")[0];
|
||||||
|
|
||||||
|
return { value: subnet, release };
|
||||||
|
} catch (e) {
|
||||||
|
await release();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNextAvailableOrgSubnet(): Promise<string> {
|
export async function getNextAvailableOrgSubnet(): Promise<{
|
||||||
return await lockManager.withLock("org-subnet-allocation", async () => {
|
value: string;
|
||||||
|
release: () => Promise<void>;
|
||||||
|
}> {
|
||||||
|
const lockKey = "org-subnet-allocation";
|
||||||
|
const acquired = await lockManager.acquireLockWithRetry(lockKey, 6000);
|
||||||
|
if (!acquired) {
|
||||||
|
throw new Error(`Failed to acquire lock: ${lockKey}`);
|
||||||
|
}
|
||||||
|
const release = () => lockManager.releaseLock(lockKey);
|
||||||
|
|
||||||
|
try {
|
||||||
const existingAddresses = await db
|
const existingAddresses = await db
|
||||||
.select({
|
.select({
|
||||||
subnet: orgs.subnet
|
subnet: orgs.subnet
|
||||||
@@ -466,8 +484,11 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
|
|||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
|
|
||||||
return subnet;
|
return { value: subnet, release };
|
||||||
});
|
} catch (e) {
|
||||||
|
await release();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateRemoteSubnets(
|
export function generateRemoteSubnets(
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ export async function pickClientDefaults(
|
|||||||
const olmId = generateId(15);
|
const olmId = generateId(15);
|
||||||
const secret = generateId(48);
|
const secret = generateId(48);
|
||||||
|
|
||||||
const newSubnet = await getNextAvailableClientSubnet(orgId);
|
const { value: newSubnet, release } =
|
||||||
|
await getNextAvailableClientSubnet(orgId);
|
||||||
|
await release(); // release immediately — this endpoint only previews the next available value
|
||||||
if (!newSubnet) {
|
if (!newSubnet) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -203,84 +203,82 @@ export async function registerNewt(
|
|||||||
|
|
||||||
let newSiteId: number | undefined;
|
let newSiteId: number | undefined;
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
const { value: newClientAddress, release: releaseSubnetLock } =
|
||||||
const newClientAddress = await getNextAvailableClientSubnet(orgId);
|
await getNextAvailableClientSubnet(orgId);
|
||||||
if (!newClientAddress) {
|
try {
|
||||||
return next(
|
await db.transaction(async (trx) => {
|
||||||
createHttpError(
|
let clientAddress = newClientAddress.split("/")[0];
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
|
||||||
"No available subnet found"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let clientAddress = newClientAddress.split("/")[0];
|
// Create the site (type "newt", name = niceId)
|
||||||
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
|
const [newSite] = await trx
|
||||||
|
.insert(sites)
|
||||||
|
.values({
|
||||||
|
orgId,
|
||||||
|
name: name || niceId,
|
||||||
|
niceId,
|
||||||
|
address: clientAddress,
|
||||||
|
type: "newt",
|
||||||
|
dockerSocketEnabled: true,
|
||||||
|
status: keyRecord.approveNewSites
|
||||||
|
? "approved"
|
||||||
|
: "pending"
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
// Create the site (type "newt", name = niceId)
|
await logsDb.insert(statusHistory).values({
|
||||||
const [newSite] = await trx
|
entityType: "site",
|
||||||
.insert(sites)
|
entityId: newSite.siteId,
|
||||||
.values({
|
orgId: orgId,
|
||||||
orgId,
|
status: "offline",
|
||||||
name: name || niceId,
|
timestamp: Math.floor(Date.now() / 1000)
|
||||||
niceId,
|
});
|
||||||
address: clientAddress,
|
|
||||||
type: "newt",
|
|
||||||
dockerSocketEnabled: true,
|
|
||||||
status: keyRecord.approveNewSites ? "approved" : "pending"
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
await logsDb.insert(statusHistory).values({
|
newSiteId = newSite.siteId;
|
||||||
entityType: "site",
|
|
||||||
entityId: newSite.siteId,
|
// Grant admin role access to the new site
|
||||||
orgId: orgId,
|
const [adminRole] = await trx
|
||||||
status: "offline",
|
.select()
|
||||||
timestamp: Math.floor(Date.now() / 1000)
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
throw new Error(`Admin role not found for org ${orgId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(roleSites).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
siteId: newSite.siteId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the newt for this site
|
||||||
|
await trx.insert(newts).values({
|
||||||
|
newtId,
|
||||||
|
secretHash,
|
||||||
|
siteId: newSite.siteId,
|
||||||
|
dateCreated: moment().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Consume the provisioning key - cascade removes siteProvisioningKeyOrg
|
||||||
|
await trx
|
||||||
|
.update(siteProvisioningKeys)
|
||||||
|
.set({
|
||||||
|
lastUsed: moment().toISOString(),
|
||||||
|
numUsed: sql`${siteProvisioningKeys.numUsed} + 1`
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
siteProvisioningKeys.siteProvisioningKeyId,
|
||||||
|
provisioningKeyId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await usageService.add(orgId, FeatureId.SITES, 1, trx);
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
newSiteId = newSite.siteId;
|
await releaseSubnetLock();
|
||||||
|
}
|
||||||
// Grant admin role access to the new site
|
|
||||||
const [adminRole] = await trx
|
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!adminRole) {
|
|
||||||
throw new Error(`Admin role not found for org ${orgId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await trx.insert(roleSites).values({
|
|
||||||
roleId: adminRole.roleId,
|
|
||||||
siteId: newSite.siteId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create the newt for this site
|
|
||||||
await trx.insert(newts).values({
|
|
||||||
newtId,
|
|
||||||
secretHash,
|
|
||||||
siteId: newSite.siteId,
|
|
||||||
dateCreated: moment().toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Consume the provisioning key - cascade removes siteProvisioningKeyOrg
|
|
||||||
await trx
|
|
||||||
.update(siteProvisioningKeys)
|
|
||||||
.set({
|
|
||||||
lastUsed: moment().toISOString(),
|
|
||||||
numUsed: sql`${siteProvisioningKeys.numUsed} + 1`
|
|
||||||
})
|
|
||||||
.where(
|
|
||||||
eq(
|
|
||||||
siteProvisioningKeys.siteProvisioningKeyId,
|
|
||||||
provisioningKeyId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await usageService.add(orgId, FeatureId.SITES, 1, trx);
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Provisioned new site (ID: ${newSiteId}) and newt (ID: ${newtId}) for org ${orgId} via provisioning key ${provisioningKeyId}`
|
`Provisioned new site (ID: ${newSiteId}) and newt (ID: ${newtId}) for org ${orgId} via provisioning key ${provisioningKeyId}`
|
||||||
|
|||||||
+230
-226
@@ -174,6 +174,7 @@ export async function createSite(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let updatedAddress = null;
|
let updatedAddress = null;
|
||||||
|
let releaseSubnetLock: (() => Promise<void>) | null = null;
|
||||||
if (address) {
|
if (address) {
|
||||||
if (!org.subnet) {
|
if (!org.subnet) {
|
||||||
return next(
|
return next(
|
||||||
@@ -244,147 +245,22 @@ export async function createSite(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const newClientAddress = await getNextAvailableClientSubnet(orgId);
|
const { value: newClientAddress, release } =
|
||||||
if (!newClientAddress) {
|
await getNextAvailableClientSubnet(orgId);
|
||||||
return next(
|
releaseSubnetLock = release;
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"No available address found"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedAddress = newClientAddress.split("/")[0];
|
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;
|
let newSite: Site | undefined;
|
||||||
await db.transaction(async (trx) => {
|
try {
|
||||||
if (type == "newt") {
|
if (subnet && exitNodeId) {
|
||||||
[newSite] = await trx
|
//make sure the subnet is in the range of the exit node if provided
|
||||||
.insert(sites)
|
const [exitNode] = await db
|
||||||
.values({
|
.select()
|
||||||
// NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT
|
.from(exitNodes)
|
||||||
orgId,
|
.where(eq(exitNodes.exitNodeId, exitNodeId));
|
||||||
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
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
logger.warn("Exit node not found");
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
@@ -393,118 +269,246 @@ export async function createSite(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasAccess) {
|
if (!exitNode.address) {
|
||||||
logger.warn("Not authorized to use this exit node");
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.FORBIDDEN,
|
HttpCode.BAD_REQUEST,
|
||||||
"Not authorized to use this exit node"
|
"Exit node has no subnet defined"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[newSite] = await trx
|
const subnetIp = subnet.split("/")[0];
|
||||||
.insert(sites)
|
|
||||||
.values({
|
if (!isIpInCidr(subnetIp, exitNode.address)) {
|
||||||
orgId,
|
return next(
|
||||||
exitNodeId,
|
createHttpError(
|
||||||
name,
|
HttpCode.BAD_REQUEST,
|
||||||
niceId: updatedNiceId!,
|
"Subnet is not in the CIDR range of the exit node address."
|
||||||
subnet,
|
)
|
||||||
type,
|
);
|
||||||
pubKey: pubKey || null,
|
}
|
||||||
status: "approved"
|
|
||||||
|
// lets also make sure there is no overlap with other sites on the exit node
|
||||||
|
const sitesQuery = await db
|
||||||
|
.select({
|
||||||
|
subnet: sites.subnet
|
||||||
})
|
})
|
||||||
.returning();
|
.from(sites)
|
||||||
} else if (type == "local") {
|
.where(
|
||||||
[newSite] = await trx
|
and(
|
||||||
.insert(sites)
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
.values({
|
eq(sites.subnet, subnet)
|
||||||
exitNodeId: exitNodeId || null,
|
)
|
||||||
orgId,
|
);
|
||||||
name,
|
|
||||||
niceId: updatedNiceId!,
|
if (sitesQuery.length > 0) {
|
||||||
type,
|
return next(
|
||||||
dockerSocketEnabled: false,
|
createHttpError(
|
||||||
online: true,
|
HttpCode.CONFLICT,
|
||||||
subnet: "0.0.0.0/32",
|
`Subnet ${subnet} overlaps with an existing site on this exit node. Please restart site creation.`
|
||||||
status: "approved"
|
)
|
||||||
})
|
);
|
||||||
.returning();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedNiceId = niceId;
|
||||||
|
if (!niceId) {
|
||||||
|
updatedNiceId = await getUniqueSiteName(orgId);
|
||||||
} else {
|
} else {
|
||||||
return next(
|
// make sure the niceId is unique
|
||||||
createHttpError(
|
const existingSite = await db
|
||||||
HttpCode.BAD_REQUEST,
|
.select()
|
||||||
"Site type not recognized"
|
.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
|
await db.transaction(async (trx) => {
|
||||||
.select()
|
if (type == "newt") {
|
||||||
.from(roles)
|
[newSite] = await trx
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
.insert(sites)
|
||||||
.limit(1);
|
.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) {
|
await logsDb.insert(statusHistory).values({
|
||||||
return next(
|
entityType: "site",
|
||||||
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
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({
|
if (!exitNodeId) {
|
||||||
roleId: adminRole[0].roleId,
|
return next(
|
||||||
siteId: newSite.siteId
|
createHttpError(
|
||||||
});
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Exit node ID is required for tunneled sites"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
const { exitNode, hasAccess } =
|
||||||
req.user &&
|
await verifyExitNodeOrgAccess(exitNodeId, orgId);
|
||||||
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
|
|
||||||
) {
|
if (!exitNode) {
|
||||||
// make sure the user can access the site
|
logger.warn("Exit node not found");
|
||||||
trx.insert(userSites).values({
|
return next(
|
||||||
userId: req.user?.userId!,
|
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
|
siteId: newSite.siteId
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// add the peer to the exit node
|
if (
|
||||||
if (type == "newt") {
|
req.user &&
|
||||||
const secretHash = await hashPassword(updatedNewtSecret);
|
!req.userOrgRoleIds?.includes(adminRole[0].roleId)
|
||||||
|
) {
|
||||||
await trx.insert(newts).values({
|
// make sure the user can access the site
|
||||||
newtId: updatedNewtId,
|
trx.insert(userSites).values({
|
||||||
secretHash,
|
userId: req.user?.userId!,
|
||||||
siteId: newSite.siteId,
|
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) {
|
// add the peer to the exit node
|
||||||
return next(
|
if (type == "newt") {
|
||||||
createHttpError(
|
const secretHash = await hashPassword(updatedNewtSecret);
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"Exit node ID is required for wireguard sites"
|
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, {
|
await usageService.add(orgId, FeatureId.SITES, 1, trx);
|
||||||
publicKey: pubKey,
|
});
|
||||||
allowedIps: []
|
} finally {
|
||||||
});
|
await releaseSubnetLock?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
await usageService.add(orgId, FeatureId.SITES, 1, trx);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!newSite) {
|
if (!newSite) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
@@ -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) {
|
if (!newClientAddress) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -397,144 +397,163 @@ export async function createSiteResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let aliasAddress: string | null = null;
|
let aliasAddress: string | null = null;
|
||||||
|
let releaseAliasLock: (() => Promise<void>) | null = null;
|
||||||
if (mode === "host" || mode === "http") {
|
if (mode === "host" || mode === "http") {
|
||||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
const { value, release } =
|
||||||
|
await getNextAvailableAliasAddress(orgId);
|
||||||
|
aliasAddress = value;
|
||||||
|
releaseAliasLock = release;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newSiteResource: SiteResource | undefined;
|
let newSiteResource: SiteResource | undefined;
|
||||||
await db.transaction(async (trx) => {
|
try {
|
||||||
const [network] = await trx
|
await db.transaction(async (trx) => {
|
||||||
.insert(networks)
|
const [network] = await trx
|
||||||
.values({
|
.insert(networks)
|
||||||
scope: "resource",
|
.values({
|
||||||
orgId: orgId
|
scope: "resource",
|
||||||
})
|
orgId: orgId
|
||||||
.returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
if (!network) {
|
if (!network) {
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Failed to create network`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
|
||||||
if (mode === "http") {
|
|
||||||
tcpPortRangeStringAdjusted = "443,80";
|
|
||||||
} else if (mode === "ssh") {
|
|
||||||
tcpPortRangeStringAdjusted = destinationPort
|
|
||||||
? destinationPort.toString()
|
|
||||||
: "22";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the site resource
|
|
||||||
const insertValues: typeof siteResources.$inferInsert = {
|
|
||||||
niceId: updatedNiceId!,
|
|
||||||
orgId,
|
|
||||||
name,
|
|
||||||
mode,
|
|
||||||
ssl,
|
|
||||||
networkId: network.networkId,
|
|
||||||
destination: destination, // the ssh can be null
|
|
||||||
scheme,
|
|
||||||
destinationPort,
|
|
||||||
enabled,
|
|
||||||
alias: alias ? alias.trim() : null,
|
|
||||||
aliasAddress,
|
|
||||||
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
|
||||||
udpPortRangeString:
|
|
||||||
mode == "http" || mode == "ssh" ? "" : udpPortRangeString,
|
|
||||||
disableIcmp:
|
|
||||||
disableIcmp ||
|
|
||||||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
|
|
||||||
domainId,
|
|
||||||
subdomain: finalSubdomain,
|
|
||||||
fullDomain
|
|
||||||
};
|
|
||||||
if (isLicensedSshPam) {
|
|
||||||
if (authDaemonPort !== undefined)
|
|
||||||
insertValues.authDaemonPort = authDaemonPort;
|
|
||||||
if (authDaemonMode !== undefined)
|
|
||||||
insertValues.authDaemonMode = authDaemonMode;
|
|
||||||
if (pamMode !== undefined) insertValues.pamMode = pamMode;
|
|
||||||
}
|
|
||||||
[newSiteResource] = await trx
|
|
||||||
.insert(siteResources)
|
|
||||||
.values(insertValues)
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
const siteResourceId = newSiteResource.siteResourceId;
|
|
||||||
|
|
||||||
//////////////////// update the associations ////////////////////
|
|
||||||
|
|
||||||
for (const siteId of siteIds) {
|
|
||||||
await trx.insert(siteNetworks).values({
|
|
||||||
siteId: siteId,
|
|
||||||
networkId: network.networkId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const [adminRole] = await trx
|
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!adminRole) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await trx.insert(roleSiteResources).values({
|
|
||||||
roleId: adminRole.roleId,
|
|
||||||
siteResourceId: siteResourceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (roleIds.length > 0) {
|
|
||||||
await trx
|
|
||||||
.insert(roleSiteResources)
|
|
||||||
.values(
|
|
||||||
roleIds.map((roleId) => ({ roleId, siteResourceId }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userIds.length > 0) {
|
|
||||||
await trx
|
|
||||||
.insert(userSiteResources)
|
|
||||||
.values(
|
|
||||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientIds.length > 0) {
|
|
||||||
await trx.insert(clientSiteResources).values(
|
|
||||||
clientIds.map((clientId) => ({
|
|
||||||
clientId,
|
|
||||||
siteResourceId
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const siteToAssign of sitesToAssign) {
|
|
||||||
const [newt] = await trx
|
|
||||||
.select()
|
|
||||||
.from(newts)
|
|
||||||
.where(eq(newts.siteId, siteToAssign.siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!newt) {
|
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
`Newt not found for site ${siteToAssign.siteId}`
|
`Failed to create network`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
||||||
|
if (mode === "http") {
|
||||||
|
tcpPortRangeStringAdjusted = "443,80";
|
||||||
|
} else if (mode === "ssh") {
|
||||||
|
tcpPortRangeStringAdjusted = destinationPort
|
||||||
|
? destinationPort.toString()
|
||||||
|
: "22";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the site resource
|
||||||
|
const insertValues: typeof siteResources.$inferInsert = {
|
||||||
|
niceId: updatedNiceId!,
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
mode,
|
||||||
|
ssl,
|
||||||
|
networkId: network.networkId,
|
||||||
|
destination: destination, // the ssh can be null
|
||||||
|
scheme,
|
||||||
|
destinationPort,
|
||||||
|
enabled,
|
||||||
|
alias: alias ? alias.trim() : null,
|
||||||
|
aliasAddress,
|
||||||
|
tcpPortRangeString: tcpPortRangeStringAdjusted,
|
||||||
|
udpPortRangeString:
|
||||||
|
mode == "http" || mode == "ssh"
|
||||||
|
? ""
|
||||||
|
: udpPortRangeString,
|
||||||
|
disableIcmp:
|
||||||
|
disableIcmp ||
|
||||||
|
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
|
||||||
|
domainId,
|
||||||
|
subdomain: finalSubdomain,
|
||||||
|
fullDomain
|
||||||
|
};
|
||||||
|
if (isLicensedSshPam) {
|
||||||
|
if (authDaemonPort !== undefined)
|
||||||
|
insertValues.authDaemonPort = authDaemonPort;
|
||||||
|
if (authDaemonMode !== undefined)
|
||||||
|
insertValues.authDaemonMode = authDaemonMode;
|
||||||
|
if (pamMode !== undefined) insertValues.pamMode = pamMode;
|
||||||
|
}
|
||||||
|
[newSiteResource] = await trx
|
||||||
|
.insert(siteResources)
|
||||||
|
.values(insertValues)
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const siteResourceId = newSiteResource.siteResourceId;
|
||||||
|
|
||||||
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
|
for (const siteId of siteIds) {
|
||||||
|
await trx.insert(siteNetworks).values({
|
||||||
|
siteId: siteId,
|
||||||
|
networkId: network.networkId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [adminRole] = await trx
|
||||||
|
.select()
|
||||||
|
.from(roles)
|
||||||
|
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Admin role not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx.insert(roleSiteResources).values({
|
||||||
|
roleId: adminRole.roleId,
|
||||||
|
siteResourceId: siteResourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (roleIds.length > 0) {
|
||||||
|
await trx
|
||||||
|
.insert(roleSiteResources)
|
||||||
|
.values(
|
||||||
|
roleIds.map((roleId) => ({
|
||||||
|
roleId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
await trx
|
||||||
|
.insert(userSiteResources)
|
||||||
|
.values(
|
||||||
|
userIds.map((userId) => ({
|
||||||
|
userId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientIds.length > 0) {
|
||||||
|
await trx.insert(clientSiteResources).values(
|
||||||
|
clientIds.map((clientId) => ({
|
||||||
|
clientId,
|
||||||
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const siteToAssign of sitesToAssign) {
|
||||||
|
const [newt] = await trx
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.siteId, siteToAssign.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!newt) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Newt not found for site ${siteToAssign.siteId}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
await releaseAliasLock?.();
|
||||||
|
}
|
||||||
|
|
||||||
if (!newSiteResource) {
|
if (!newSiteResource) {
|
||||||
return next(
|
return next(
|
||||||
|
|||||||
Reference in New Issue
Block a user