mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-28 11:43:03 +00:00
Properly lock the ip selection through writes to db
This commit is contained in:
@@ -51,7 +51,9 @@ export async function pickClientDefaults(
|
||||
const olmId = generateId(15);
|
||||
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) {
|
||||
return next(
|
||||
createHttpError(
|
||||
|
||||
@@ -203,84 +203,82 @@ export async function registerNewt(
|
||||
|
||||
let newSiteId: number | undefined;
|
||||
|
||||
await db.transaction(async (trx) => {
|
||||
const newClientAddress = await getNextAvailableClientSubnet(orgId);
|
||||
if (!newClientAddress) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"No available subnet found"
|
||||
)
|
||||
);
|
||||
}
|
||||
const { value: newClientAddress, release: releaseSubnetLock } =
|
||||
await getNextAvailableClientSubnet(orgId);
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
let clientAddress = newClientAddress.split("/")[0];
|
||||
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
|
||||
|
||||
let clientAddress = newClientAddress.split("/")[0];
|
||||
clientAddress = `${clientAddress}/${org.subnet!.split("/")[1]}`; // we want the block size of the whole org
|
||||
// Create the site (type "newt", name = niceId)
|
||||
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)
|
||||
const [newSite] = await trx
|
||||
.insert(sites)
|
||||
.values({
|
||||
orgId,
|
||||
name: name || niceId,
|
||||
niceId,
|
||||
address: clientAddress,
|
||||
type: "newt",
|
||||
dockerSocketEnabled: true,
|
||||
status: keyRecord.approveNewSites ? "approved" : "pending"
|
||||
})
|
||||
.returning();
|
||||
await logsDb.insert(statusHistory).values({
|
||||
entityType: "site",
|
||||
entityId: newSite.siteId,
|
||||
orgId: orgId,
|
||||
status: "offline",
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
|
||||
await logsDb.insert(statusHistory).values({
|
||||
entityType: "site",
|
||||
entityId: newSite.siteId,
|
||||
orgId: orgId,
|
||||
status: "offline",
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
newSiteId = newSite.siteId;
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
newSiteId = newSite.siteId;
|
||||
|
||||
// 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);
|
||||
});
|
||||
} finally {
|
||||
await releaseSubnetLock();
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`Provisioned new site (ID: ${newSiteId}) and newt (ID: ${newtId}) for org ${orgId} via provisioning key ${provisioningKeyId}`
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -397,144 +397,163 @@ export async function createSiteResource(
|
||||
}
|
||||
|
||||
let aliasAddress: string | null = null;
|
||||
let releaseAliasLock: (() => Promise<void>) | null = null;
|
||||
if (mode === "host" || mode === "http") {
|
||||
aliasAddress = await getNextAvailableAliasAddress(orgId);
|
||||
const { value, release } =
|
||||
await getNextAvailableAliasAddress(orgId);
|
||||
aliasAddress = value;
|
||||
releaseAliasLock = release;
|
||||
}
|
||||
|
||||
let newSiteResource: SiteResource | undefined;
|
||||
await db.transaction(async (trx) => {
|
||||
const [network] = await trx
|
||||
.insert(networks)
|
||||
.values({
|
||||
scope: "resource",
|
||||
orgId: orgId
|
||||
})
|
||||
.returning();
|
||||
try {
|
||||
await db.transaction(async (trx) => {
|
||||
const [network] = await trx
|
||||
.insert(networks)
|
||||
.values({
|
||||
scope: "resource",
|
||||
orgId: orgId
|
||||
})
|
||||
.returning();
|
||||
|
||||
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) {
|
||||
if (!network) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Newt not found for site ${siteToAssign.siteId}`
|
||||
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(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Newt not found for site ${siteToAssign.siteId}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
await releaseAliasLock?.();
|
||||
}
|
||||
|
||||
if (!newSiteResource) {
|
||||
return next(
|
||||
|
||||
Reference in New Issue
Block a user