diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 6168f85d..ae2ea4af 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -1,4 +1,4 @@ -import { db, newts, blueprints, Blueprint } from "@server/db"; +import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db"; import { Config, ConfigSchema } from "./types"; import { ProxyResourcesResults, updateProxyResources } from "./proxyResources"; import { fromError } from "zod-validation-error"; @@ -15,6 +15,7 @@ import { BlueprintSource } from "@server/routers/blueprints/types"; import { stringify as stringifyYaml } from "yaml"; import { faker } from "@faker-js/faker"; import { handleMessagingForUpdatedSiteResource } from "@server/routers/siteResource"; +import { rebuildClientAssociationsFromSiteResource } from "../rebuildClientAssociations"; type ApplyBlueprintArgs = { orgId: string; @@ -108,38 +109,139 @@ export async function applyBlueprint({ // We need to update the targets on the newts from the successfully updated information for (const result of clientResourcesResults) { - const [site] = await trx - .select() - .from(sites) - .innerJoin(newts, eq(sites.siteId, newts.siteId)) - .where( - and( - eq(sites.siteId, result.newSiteResource.siteId), - eq(sites.orgId, orgId), - eq(sites.type, "newt"), - isNotNull(sites.pubKey) + if ( + result.oldSiteResource && + result.oldSiteResource.siteId != + result.newSiteResource.siteId + ) { + // the site resource has moved sites + // insert it first so we get a new siteResourceId just in case + const [insertedSiteResource] = await trx + .insert(siteResources) + .values({ + ...result.oldSiteResource, + siteResourceId: undefined // to generate a new one + }) + .returning(); + + // query existing associations + const existingRoleIds = await trx + .select() + .from(roleSiteResources) + .where( + eq( + roleSiteResources.siteResourceId, + result.oldSiteResource.siteResourceId + ) ) - ) - .limit(1); + .then((rows) => rows.map((row) => row.roleId)); - if (!site) { - logger.debug( - `No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update` + const existingUserIds= await trx + .select() + .from(userSiteResources) + .where( + eq( + userSiteResources.siteResourceId, + result.oldSiteResource.siteResourceId + ) + ).then((rows) => rows.map((row) => row.userId)); + + const existingClientIds = await trx + .select() + .from(clientSiteResources) + .where( + eq( + clientSiteResources.siteResourceId, + result.oldSiteResource.siteResourceId + ) + ).then((rows) => rows.map((row) => row.clientId)); + + // delete the existing site resource + await trx + .delete(siteResources) + .where( + and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId)) + ); + + await rebuildClientAssociationsFromSiteResource( + result.oldSiteResource, + trx + ); + + // wait some time to allow for messages to be handled + await new Promise((resolve) => setTimeout(resolve, 750)); + + //////////////////// update the associations //////////////////// + + if (existingRoleIds.length > 0) { + await trx.insert(roleSiteResources).values( + existingRoleIds.map((roleId) => ({ + roleId, + siteResourceId: insertedSiteResource!.siteResourceId + })) + ); + } + + if (existingUserIds.length > 0) { + await trx.insert(userSiteResources).values( + existingUserIds.map((userId) => ({ + userId, + siteResourceId: insertedSiteResource!.siteResourceId + })) + ); + } + + if (existingClientIds.length > 0) { + await trx.insert(clientSiteResources).values( + existingClientIds.map((clientId) => ({ + clientId, + siteResourceId: insertedSiteResource!.siteResourceId + })) + ); + } + + await rebuildClientAssociationsFromSiteResource( + insertedSiteResource, + trx + ); + + } else { + const [newSite] = await trx + .select() + .from(sites) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) + .where( + and( + eq(sites.siteId, result.newSiteResource.siteId), + eq(sites.orgId, orgId), + eq(sites.type, "newt"), + isNotNull(sites.pubKey) + ) + ) + .limit(1); + + if (!newSite) { + logger.debug( + `No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update` + ); + continue; + } + + logger.debug( + `Updating client resource ${result.newSiteResource.siteResourceId} on site ${newSite.sites.siteId}` + ); + + await handleMessagingForUpdatedSiteResource( + result.oldSiteResource, + result.newSiteResource, + { + siteId: newSite.sites.siteId, + orgId: newSite.sites.orgId + }, + trx ); - continue; } - logger.debug( - `Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}` - ); - - await handleMessagingForUpdatedSiteResource( - result.oldSiteResource, - result.newSiteResource, - { siteId: site.sites.siteId, orgId: site.sites.orgId }, - trx - ); - // await addClientTargets( // site.newt.newtId, // result.resource.destination, diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index 3d1e70cc..0abc7d73 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -12,9 +12,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; const deleteSiteResourceParamsSchema = z.strictObject({ - siteResourceId: z.string().transform(Number).pipe(z.int().positive()), - siteId: z.string().transform(Number).pipe(z.int().positive()), - orgId: z.string() + siteResourceId: z.string().transform(Number).pipe(z.int().positive()) }); export type DeleteSiteResourceResponse = { @@ -23,7 +21,7 @@ export type DeleteSiteResourceResponse = { registry.registerPath({ method: "delete", - path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + path: "/site-resource/{siteResourceId}", description: "Delete a site resource.", tags: [OpenAPITags.Client, OpenAPITags.Org], request: { @@ -50,29 +48,13 @@ export async function deleteSiteResource( ); } - const { siteResourceId, siteId, orgId } = parsedParams.data; - - const [site] = await db - .select() - .from(sites) - .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) - .limit(1); - - if (!site) { - return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); - } + const { siteResourceId } = parsedParams.data; // Check if site resource exists const [existingSiteResource] = await db .select() .from(siteResources) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) + .where(and(eq(siteResources.siteResourceId, siteResourceId))) .limit(1); if (!existingSiteResource) { @@ -85,19 +67,13 @@ export async function deleteSiteResource( // Delete the site resource const [removedSiteResource] = await trx .delete(siteResources) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) - ) - ) + .where(and(eq(siteResources.siteResourceId, siteResourceId))) .returning(); const [newt] = await trx .select() .from(newts) - .where(eq(newts.siteId, site.siteId)) + .where(eq(newts.siteId, removedSiteResource.siteId)) .limit(1); if (!newt) { @@ -113,7 +89,7 @@ export async function deleteSiteResource( }); logger.info( - `Deleted site resource ${siteResourceId} for site ${siteId}` + `Deleted site resource ${siteResourceId}` ); return response(res, { diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 44b2bc33..dbe668b5 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -196,6 +196,27 @@ export async function updateSiteResource( ); } + let existingSite = site; + let siteChanged = false; + if (existingSiteResource.siteId !== siteId) { + siteChanged = true; + // get the existing site + [existingSite] = await db + .select() + .from(sites) + .where(eq(sites.siteId, existingSiteResource.siteId)) + .limit(1); + + if (!existingSite) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Existing site not found" + ) + ); + } + } + // make sure the alias is unique within the org if provided if (alias) { const [conflict] = await db @@ -222,95 +243,222 @@ export async function updateSiteResource( let updatedSiteResource: SiteResource | undefined; await db.transaction(async (trx) => { - // Update the site resource - [updatedSiteResource] = await trx - .update(siteResources) - .set({ - name: name, - siteId: siteId, - mode: mode, - destination: destination, - enabled: enabled, - alias: alias && alias.trim() ? alias : null, - tcpPortRangeString: tcpPortRangeString, - udpPortRangeString: udpPortRangeString, - disableIcmp: disableIcmp - }) - .where(and(eq(siteResources.siteResourceId, siteResourceId))) - .returning(); + // if the site is changed we need to delete and recreate the resource to avoid complications with the rebuild function otherwise we can just update in place + if (siteChanged) { + // create the new site resource from the removed one with the new siteId and updated fields + // insert it first so we get a new siteResourceId just in case + const [insertedSiteResource] = await trx + .insert(siteResources) + .values({ + ...existingSiteResource, + siteResourceId: undefined // to generate a new one + }) + .returning(); - //////////////////// update the associations //////////////////// - - await trx - .delete(clientSiteResources) - .where(eq(clientSiteResources.siteResourceId, siteResourceId)); - - if (clientIds.length > 0) { - await trx.insert(clientSiteResources).values( - clientIds.map((clientId) => ({ - clientId, - siteResourceId - })) - ); - } - - await trx - .delete(userSiteResources) - .where(eq(userSiteResources.siteResourceId, siteResourceId)); - - if (userIds.length > 0) { + // delete the existing site resource await trx - .insert(userSiteResources) - .values( - userIds.map((userId) => ({ userId, siteResourceId })) + .delete(siteResources) + .where( + and(eq(siteResources.siteResourceId, siteResourceId)) ); - } - // Get all admin role IDs for this org to exclude from deletion - const adminRoles = await trx - .select() - .from(roles) - .where( - and( - eq(roles.isAdmin, true), - eq(roles.orgId, updatedSiteResource.orgId) - ) + await rebuildClientAssociationsFromSiteResource( + existingSiteResource, + trx ); - const adminRoleIds = adminRoles.map((role) => role.roleId); - if (adminRoleIds.length > 0) { - await trx.delete(roleSiteResources).where( - and( - eq(roleSiteResources.siteResourceId, siteResourceId), - ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role + // wait some time to allow for messages to be handled + await new Promise((resolve) => setTimeout(resolve, 750)); + + [updatedSiteResource] = await trx + .update(siteResources) + .set({ + name: name, + siteId: siteId, + mode: mode, + destination: destination, + enabled: enabled, + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString, + disableIcmp: disableIcmp + }) + .where( + and( + eq( + siteResources.siteResourceId, + insertedSiteResource.siteResourceId + ) + ) ) + .returning(); + + if (!updatedSiteResource) { + throw new Error( + "Failed to create updated site resource after site change" + ); + } + + //////////////////// update the associations //////////////////// + + const [adminRole] = await trx + .select() + .from(roles) + .where( + and( + eq(roles.isAdmin, true), + eq(roles.orgId, updatedSiteResource.orgId) + ) + ) + .limit(1); + + if (!adminRole) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Admin role not found` + ) + ); + } + + await trx.insert(roleSiteResources).values({ + roleId: adminRole.roleId, + siteResourceId: updatedSiteResource.siteResourceId + }); + + if (roleIds.length > 0) { + await trx.insert(roleSiteResources).values( + roleIds.map((roleId) => ({ + roleId, + siteResourceId: updatedSiteResource!.siteResourceId + })) + ); + } + + if (userIds.length > 0) { + await trx.insert(userSiteResources).values( + userIds.map((userId) => ({ + userId, + siteResourceId: updatedSiteResource!.siteResourceId + })) + ); + } + + if (clientIds.length > 0) { + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId: updatedSiteResource!.siteResourceId + })) + ); + } + + await rebuildClientAssociationsFromSiteResource( + updatedSiteResource, + trx ); } else { - await trx - .delete(roleSiteResources) + // Update the site resource + [updatedSiteResource] = await trx + .update(siteResources) + .set({ + name: name, + siteId: siteId, + mode: mode, + destination: destination, + enabled: enabled, + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString, + disableIcmp: disableIcmp + }) .where( - eq(roleSiteResources.siteResourceId, siteResourceId) - ); - } + and(eq(siteResources.siteResourceId, siteResourceId)) + ) + .returning(); + + //////////////////// update the associations //////////////////// - if (roleIds.length > 0) { await trx - .insert(roleSiteResources) - .values( - roleIds.map((roleId) => ({ roleId, siteResourceId })) + .delete(clientSiteResources) + .where( + eq(clientSiteResources.siteResourceId, siteResourceId) ); + + if (clientIds.length > 0) { + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + + await trx + .delete(userSiteResources) + .where( + eq(userSiteResources.siteResourceId, siteResourceId) + ); + + if (userIds.length > 0) { + await trx.insert(userSiteResources).values( + userIds.map((userId) => ({ + userId, + siteResourceId + })) + ); + } + + // Get all admin role IDs for this org to exclude from deletion + const adminRoles = await trx + .select() + .from(roles) + .where( + and( + eq(roles.isAdmin, true), + eq(roles.orgId, updatedSiteResource.orgId) + ) + ); + const adminRoleIds = adminRoles.map((role) => role.roleId); + + if (adminRoleIds.length > 0) { + await trx.delete(roleSiteResources).where( + and( + eq( + roleSiteResources.siteResourceId, + siteResourceId + ), + ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role + ) + ); + } else { + await trx + .delete(roleSiteResources) + .where( + eq(roleSiteResources.siteResourceId, siteResourceId) + ); + } + + if (roleIds.length > 0) { + await trx.insert(roleSiteResources).values( + roleIds.map((roleId) => ({ + roleId, + siteResourceId + })) + ); + } + + logger.info( + `Updated site resource ${siteResourceId} for site ${siteId}` + ); + + await handleMessagingForUpdatedSiteResource( + existingSiteResource, + updatedSiteResource, + { siteId: site.siteId, orgId: site.orgId }, + trx + ); } - - logger.info( - `Updated site resource ${siteResourceId} for site ${siteId}` - ); - - await handleMessagingForUpdatedSiteResource( - existingSiteResource, - updatedSiteResource!, - { siteId: site.siteId, orgId: site.orgId }, - trx - ); }); return response(res, {