From 66fc8529c24d55e1a1301872aa078d07039be18f Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 6 Dec 2025 17:32:49 -0500 Subject: [PATCH] Update blueprints to support new clients --- server/lib/blueprints/applyBlueprint.ts | 182 ++++++------- server/lib/blueprints/clientResources.ts | 234 +++++++++++++++-- server/lib/blueprints/proxyResources.ts | 28 +- server/lib/blueprints/types.ts | 177 ++++++++++--- .../siteResource/updateSiteResource.ts | 239 +++++++++--------- 5 files changed, 578 insertions(+), 282 deletions(-) diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 7b4f3b22..86a18c8c 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -14,6 +14,7 @@ import { 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"; type ApplyBlueprintArgs = { orgId: string; @@ -57,22 +58,63 @@ export async function applyBlueprint({ trx, siteId ); - }); - logger.debug( - `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}` - ); + logger.debug( + `Successfully updated proxy resources for org ${orgId}: ${JSON.stringify(proxyResourcesResults)}` + ); - // We need to update the targets on the newts from the successfully updated information - for (const result of proxyResourcesResults) { - for (const target of result.targetsToUpdate) { - const [site] = await db + // We need to update the targets on the newts from the successfully updated information + for (const result of proxyResourcesResults) { + for (const target of result.targetsToUpdate) { + const [site] = await trx + .select() + .from(sites) + .innerJoin(newts, eq(sites.siteId, newts.siteId)) + .where( + and( + eq(sites.siteId, target.siteId), + eq(sites.orgId, orgId), + eq(sites.type, "newt"), + isNotNull(sites.pubKey) + ) + ) + .limit(1); + + if (site) { + logger.debug( + `Updating target ${target.targetId} on site ${site.sites.siteId}` + ); + + // see if you can find a matching target health check from the healthchecksToUpdate array + const matchingHealthcheck = + result.healthchecksToUpdate.find( + (hc) => hc.targetId === target.targetId + ); + + await addProxyTargets( + site.newt.newtId, + [target], + matchingHealthcheck ? [matchingHealthcheck] : [], + result.proxyResource.protocol, + result.proxyResource.proxyPort + ); + } + } + } + + logger.debug( + `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}` + ); + + // 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, target.siteId), + eq(sites.siteId, result.newSiteResource.siteId), eq(sites.orgId, orgId), eq(sites.type, "newt"), isNotNull(sites.pubKey) @@ -80,60 +122,36 @@ export async function applyBlueprint({ ) .limit(1); - if (site) { + if (!site) { logger.debug( - `Updating target ${target.targetId} on site ${site.sites.siteId}` + `No newt site found for client resource ${result.newSiteResource.siteResourceId}, skipping target update` ); + continue; + } - // see if you can find a matching target health check from the healthchecksToUpdate array - const matchingHealthcheck = - result.healthchecksToUpdate.find( - (hc) => hc.targetId === target.targetId - ); + logger.debug( + `Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}` + ); - await addProxyTargets( - site.newt.newtId, - [target], - matchingHealthcheck ? [matchingHealthcheck] : [], - result.proxyResource.protocol, - result.proxyResource.proxyPort + if (result.oldSiteResource) { + // this was an update + await handleMessagingForUpdatedSiteResource( + result.oldSiteResource, + result.newSiteResource, + { siteId: site.sites.siteId, orgId: site.sites.orgId }, + trx ); } + + // await addClientTargets( + // site.newt.newtId, + // result.resource.destination, + // result.resource.destinationPort, + // result.resource.protocol, + // result.resource.proxyPort + // ); } - } - - logger.debug( - `Successfully updated client resources for org ${orgId}: ${JSON.stringify(clientResourcesResults)}` - ); - - // We need to update the targets on the newts from the successfully updated information - for (const result of clientResourcesResults) { - const [site] = await db - .select() - .from(sites) - .innerJoin(newts, eq(sites.siteId, newts.siteId)) - .where( - and( - eq(sites.siteId, result.resource.siteId), - eq(sites.orgId, orgId), - eq(sites.type, "newt"), - isNotNull(sites.pubKey) - ) - ) - .limit(1); - - logger.debug( - `Updating client resource ${result.resource.siteResourceId} on site ${site.sites.siteId}` - ); - - // await addClientTargets( - // site.newt.newtId, - // result.resource.destination, - // result.resource.destinationPort, - // result.resource.protocol, - // result.resource.proxyPort - // ); - } + }); blueprintSucceeded = true; blueprintMessage = "Blueprint applied successfully"; @@ -170,54 +188,4 @@ export async function applyBlueprint({ } return blueprint; -} - -// await updateDatabaseFromConfig("org_i21aifypnlyxur2", { -// resources: { -// "resource-nice-id": { -// name: "this is my resource", -// protocol: "http", -// "full-domain": "level1.test.example.com", -// "host-header": "example.com", -// "tls-server-name": "example.com", -// auth: { -// pincode: 123456, -// password: "sadfasdfadsf", -// "sso-enabled": true, -// "sso-roles": ["Member"], -// "sso-users": ["owen@pangolin.net"], -// "whitelist-users": ["owen@pangolin.net"] -// }, -// targets: [ -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// method: "http", -// port: 8000, -// healthcheck: { -// port: 8000, -// hostname: "localhost" -// } -// }, -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// method: "http", -// port: 8001 -// } -// ] -// }, -// "resource-nice-id2": { -// name: "http server", -// protocol: "tcp", -// "proxy-port": 3000, -// targets: [ -// { -// site: "glossy-plains-viscacha-rat", -// hostname: "localhost", -// port: 3000, -// } -// ] -// } -// } -// }); +} \ No newline at end of file diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index 7b92ba21..ab65336d 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -1,17 +1,23 @@ import { + clients, + clientSiteResources, + roles, + roleSiteResources, SiteResource, siteResources, Transaction, + userOrgs, + users, + userSiteResources } from "@server/db"; import { sites } from "@server/db"; -import { eq, and } from "drizzle-orm"; -import { - Config, -} from "./types"; +import { eq, and, ne, inArray } from "drizzle-orm"; +import { Config } from "./types"; import logger from "@server/logger"; export type ClientResourcesResults = { - resource: SiteResource; + newSiteResource: SiteResource; + oldSiteResource?: SiteResource; }[]; export async function updateClientResources( @@ -69,17 +75,22 @@ export async function updateClientResources( } if (existingResource) { + if (existingResource.siteId !== site.siteId) { + throw new Error( + `You can not change the site of an existing client resource (${resourceNiceId}). Please delete and recreate it instead.` + ); + } + // Update existing resource const [updatedResource] = await trx .update(siteResources) .set({ name: resourceData.name || resourceNiceId, - siteId: site.siteId, - mode: "port", - proxyPort: resourceData["proxy-port"]!, - destination: resourceData.hostname, - destinationPort: resourceData["internal-port"], - protocol: resourceData.protocol + mode: resourceData.mode, + destination: resourceData.destination, + enabled: true, // hardcoded for now + // enabled: resourceData.enabled ?? true, + alias: resourceData.alias || null }) .where( eq( @@ -89,7 +100,110 @@ export async function updateClientResources( ) .returning(); - results.push({ resource: updatedResource }); + const siteResourceId = existingResource.siteResourceId; + const orgId = existingResource.orgId; + + await trx + .delete(clientSiteResources) + .where(eq(clientSiteResources.siteResourceId, siteResourceId)); + + if (resourceData.machines.length > 0) { + // get clientIds from niceIds + const clientsToUpdate = await trx + .select() + .from(clients) + .where( + and( + inArray(clients.niceId, resourceData.machines), + eq(clients.orgId, orgId) + ) + ); + + const clientIds = clientsToUpdate.map( + (client) => client.clientId + ); + + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + + await trx + .delete(userSiteResources) + .where(eq(userSiteResources.siteResourceId, siteResourceId)); + + if (resourceData.users.length > 0) { + // get userIds from username + const usersToUpdate = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where( + and( + inArray(users.username, resourceData.users), + eq(userOrgs.orgId, orgId) + ) + ); + + const userIds = usersToUpdate.map((user) => user.user.userId); + + 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, 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 (resourceData.roles.length > 0) { + // Re-add specified roles but we need to get the roleIds from the role name in the array + const rolesToUpdate = await trx + .select() + .from(roles) + .where( + and( + eq(roles.orgId, orgId), + inArray(roles.name, resourceData.roles) + ) + ); + + const roleIds = rolesToUpdate.map((role) => role.roleId); + + await trx + .insert(roleSiteResources) + .values( + roleIds.map((roleId) => ({ roleId, siteResourceId })) + ); + } + + results.push({ + newSiteResource: updatedResource, + oldSiteResource: existingResource + }); } else { // Create new resource const [newResource] = await trx @@ -99,19 +213,103 @@ export async function updateClientResources( siteId: site.siteId, niceId: resourceNiceId, name: resourceData.name || resourceNiceId, - mode: "port", - proxyPort: resourceData["proxy-port"]!, - destination: resourceData.hostname, - destinationPort: resourceData["internal-port"], - protocol: resourceData.protocol + mode: resourceData.mode, + destination: resourceData.destination, + enabled: true, // hardcoded for now + // enabled: resourceData.enabled ?? true, + alias: resourceData.alias || null }) .returning(); + const siteResourceId = newResource.siteResourceId; + + 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(roleSiteResources).values({ + roleId: adminRole.roleId, + siteResourceId: siteResourceId + }); + + if (resourceData.roles.length > 0) { + // get roleIds from role names + const rolesToUpdate = await trx + .select() + .from(roles) + .where( + and( + eq(roles.orgId, orgId), + inArray(roles.name, resourceData.roles) + ) + ); + + const roleIds = rolesToUpdate.map((role) => role.roleId); + + await trx + .insert(roleSiteResources) + .values( + roleIds.map((roleId) => ({ roleId, siteResourceId })) + ); + } + + if (resourceData.users.length > 0) { + // get userIds from username + const usersToUpdate = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where( + and( + inArray(users.username, resourceData.users), + eq(userOrgs.orgId, orgId) + ) + ); + + const userIds = usersToUpdate.map((user) => user.user.userId); + + await trx + .insert(userSiteResources) + .values( + userIds.map((userId) => ({ userId, siteResourceId })) + ); + } + + if (resourceData.machines.length > 0) { + // get clientIds from niceIds + const clientsToUpdate = await trx + .select() + .from(clients) + .where( + and( + inArray(clients.niceId, resourceData.machines), + eq(clients.orgId, orgId) + ) + ); + + const clientIds = clientsToUpdate.map( + (client) => client.clientId + ); + + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + logger.info( `Created new client resource ${newResource.name} (${newResource.siteResourceId}) for org ${orgId}` ); - results.push({ resource: newResource }); + results.push({ newSiteResource: newResource }); } } diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 9761f57d..51296158 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -221,7 +221,8 @@ export async function updateProxyResources( domainId: domain ? domain.domainId : null, enabled: resourceEnabled, sso: resourceData.auth?.["sso-enabled"] || false, - skipToIdpId: resourceData.auth?.["auto-login-idp"] || null, + skipToIdpId: + resourceData.auth?.["auto-login-idp"] || null, ssl: resourceSsl, setHostHeader: resourceData["host-header"] || null, tlsServerName: resourceData["tls-server-name"] || null, @@ -546,7 +547,8 @@ export async function updateProxyResources( if ( existingRule.action !== getRuleAction(rule.action) || existingRule.match !== rule.match.toUpperCase() || - existingRule.value !== getRuleValue(rule.match.toUpperCase(), rule.value) + existingRule.value !== + getRuleValue(rule.match.toUpperCase(), rule.value) ) { validateRule(rule); await trx @@ -554,7 +556,10 @@ export async function updateProxyResources( .set({ action: getRuleAction(rule.action), match: rule.match.toUpperCase(), - value: getRuleValue(rule.match.toUpperCase(), rule.value), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ) }) .where( eq(resourceRules.ruleId, existingRule.ruleId) @@ -566,7 +571,10 @@ export async function updateProxyResources( resourceId: existingResource.resourceId, action: getRuleAction(rule.action), match: rule.match.toUpperCase(), - value: getRuleValue(rule.match.toUpperCase(), rule.value), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ), priority: index + 1 // start priorities at 1 }); } @@ -852,16 +860,16 @@ async function syncUserResources( .from(userResources) .where(eq(userResources.resourceId, resourceId)); - for (const email of ssoUsers) { + for (const username of ssoUsers) { const [user] = await trx .select() .from(users) .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where(and(eq(users.email, email), eq(userOrgs.orgId, orgId))) + .where(and(eq(users.username, username), eq(userOrgs.orgId, orgId))) .limit(1); if (!user) { - throw new Error(`User not found: ${email} in org ${orgId}`); + throw new Error(`User not found: ${username} in org ${orgId}`); } const existingUserResource = existingUserResources.find( @@ -889,7 +897,11 @@ async function syncUserResources( ) .limit(1); - if (user && user.user.email && !ssoUsers.includes(user.user.email)) { + if ( + user && + user.user.username && + !ssoUsers.includes(user.user.username) + ) { await trx .delete(userResources) .where( diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index f3523f7a..bf513461 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -16,7 +16,11 @@ export const TargetHealthCheckSchema = z.object({ "unhealthy-interval": z.int().default(30), unhealthyInterval: z.int().optional(), // deprecated alias timeout: z.int().default(5), - headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional().default(null), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable() + .optional() + .default(null), "follow-redirects": z.boolean().default(true), followRedirects: z.boolean().optional(), // deprecated alias method: z.string().default("GET"), @@ -36,7 +40,10 @@ export const TargetSchema = z.object({ healthcheck: TargetHealthCheckSchema.optional(), rewritePath: z.string().optional(), // deprecated alias "rewrite-path": z.string().optional(), - "rewrite-match": z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), + "rewrite-match": z + .enum(["exact", "prefix", "regex", "stripPrefix"]) + .optional() + .nullable(), priority: z.int().min(1).max(1000).optional().default(100) }); export type TargetData = z.infer; @@ -45,10 +52,12 @@ export const AuthSchema = z.object({ // pincode has to have 6 digits pincode: z.number().min(100000).max(999999).optional(), password: z.string().min(1).optional(), - "basic-auth": z.object({ - user: z.string().min(1), - password: z.string().min(1) - }).optional(), + "basic-auth": z + .object({ + user: z.string().min(1), + password: z.string().min(1) + }) + .optional(), "sso-enabled": z.boolean().optional().default(false), "sso-roles": z .array(z.string()) @@ -59,7 +68,7 @@ export const AuthSchema = z.object({ }), "sso-users": z.array(z.email()).optional().default([]), "whitelist-users": z.array(z.email()).optional().default([]), - "auto-login-idp": z.int().positive().optional(), + "auto-login-idp": z.int().positive().optional() }); export const RuleSchema = z.object({ @@ -122,7 +131,6 @@ export const ResourceSchema = z { path: ["targets"], error: "When protocol is 'http', all targets must have a 'method' field" - } ) .refine( @@ -204,23 +212,122 @@ export function isTargetsOnlyResource(resource: any): boolean { return Object.keys(resource).length === 1 && resource.targets; } -export const ClientResourceSchema = z.object({ - name: z.string().min(2).max(100), - site: z.string().min(2).max(100).optional(), - protocol: z.enum(["tcp", "udp"]), - "proxy-port": z.number().min(1).max(65535), - "hostname": z.string().min(1).max(255), - "internal-port": z.number().min(1).max(65535), - enabled: z.boolean().optional().default(true) -}); +export const ClientResourceSchema = z + .object({ + name: z.string().min(1).max(255), + mode: z.enum(["host", "cidr"]), + site: z.string(), + // protocol: z.enum(["tcp", "udp"]).optional(), + // proxyPort: z.int().positive().optional(), + // destinationPort: z.int().positive().optional(), + destination: z.string().min(1), + // enabled: z.boolean().default(true), + alias: z + .string() + .regex( + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name (e.g., example.com)" + ) + .optional(), + roles: z + .array(z.string()) + .optional() + .default([]) + .refine((roles) => !roles.includes("Admin"), { + error: "Admin role cannot be included in roles" + }), + users: z.array(z.email()).optional().default([]), + machines: z.array(z.string()).optional().default([]) + }) + .refine( + (data) => { + if (data.mode === "host") { + // Check if it's a valid IP address using zod (v4 or v6) + const isValidIP = z + .union([z.ipv4(), z.ipv6()]) + .safeParse(data.destination).success; + + if (isValidIP) { + return true; + } + + // Check if it's a valid domain (hostname pattern, TLD not required) + const domainRegex = + /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; + const isValidDomain = domainRegex.test(data.destination); + const isValidAlias = data.alias && domainRegex.test(data.alias); + + return isValidDomain && isValidAlias; // require the alias to be set in the case of domain + } + return true; + }, + { + message: + "Destination must be a valid IP address or valid domain AND alias is required" + } + ) + .refine( + (data) => { + if (data.mode === "cidr") { + // Check if it's a valid CIDR (v4 or v6) + const isValidCIDR = z + .union([z.cidrv4(), z.cidrv6()]) + .safeParse(data.destination).success; + return isValidCIDR; + } + return true; + }, + { + message: "Destination must be a valid CIDR notation for cidr mode" + } + ); // Schema for the entire configuration object export const ConfigSchema = z .object({ - "proxy-resources": z.record(z.string(), ResourceSchema).optional().prefault({}), - "client-resources": z.record(z.string(), ClientResourceSchema).optional().prefault({}), + "proxy-resources": z + .record(z.string(), ResourceSchema) + .optional() + .prefault({}), + "public-resources": z + .record(z.string(), ResourceSchema) + .optional() + .prefault({}), + "client-resources": z + .record(z.string(), ClientResourceSchema) + .optional() + .prefault({}), + "private-resources": z + .record(z.string(), ClientResourceSchema) + .optional() + .prefault({}), sites: z.record(z.string(), SiteSchema).optional().prefault({}) }) + .transform((data) => { + // Merge public-resources into proxy-resources + if (data["public-resources"]) { + data["proxy-resources"] = { + ...data["proxy-resources"], + ...data["public-resources"] + }; + delete (data as any)["public-resources"]; + } + + // Merge private-resources into client-resources + if (data["private-resources"]) { + data["client-resources"] = { + ...data["client-resources"], + ...data["private-resources"] + }; + delete (data as any)["private-resources"]; + } + + return data as { + "proxy-resources": Record>; + "client-resources": Record>; + sites: Record>; + }; + }) .refine( // Enforce the full-domain uniqueness across resources in the same stack (config) => { @@ -278,12 +385,10 @@ export const ConfigSchema = z const duplicates = Array.from(protocolPortMap.entries()) .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([protocolPort, resourceKeys]) => { - const [protocol, port] = protocolPort.split(':'); - return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`; - } - ) + .map(([protocolPort, resourceKeys]) => { + const [protocol, port] = protocolPort.split(":"); + return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`; + }) .join("; "); if (duplicates.length !== 0) { @@ -295,35 +400,35 @@ export const ConfigSchema = z } ) .refine( - // Enforce proxy-port uniqueness within client-resources + // Enforce alias uniqueness within client-resources (config) => { // Extract duplicates for error message - const proxyPortMap = new Map(); + const aliasMap = new Map(); Object.entries(config["client-resources"]).forEach( ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - if (proxyPort !== undefined) { - if (!proxyPortMap.has(proxyPort)) { - proxyPortMap.set(proxyPort, []); + const alias = resource.alias; + if (alias !== undefined) { + if (!aliasMap.has(alias)) { + aliasMap.set(alias, []); } - proxyPortMap.get(proxyPort)!.push(resourceKey); + aliasMap.get(alias)!.push(resourceKey); } } ); - const duplicates = Array.from(proxyPortMap.entries()) + const duplicates = Array.from(aliasMap.entries()) .filter(([_, resourceKeys]) => resourceKeys.length > 1) .map( - ([proxyPort, resourceKeys]) => - `port ${proxyPort} used by client-resources: ${resourceKeys.join(", ")}` + ([alias, resourceKeys]) => + `alias '${alias}' used by client-resources: ${resourceKeys.join(", ")}` ) .join("; "); if (duplicates.length !== 0) { return { path: ["client-resources"], - error: `Duplicate 'proxy-port' values found in client-resources: ${duplicates}` + error: `Duplicate 'alias' values found in client-resources: ${duplicates}` }; } } diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 857080b0..9161c509 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -8,6 +8,7 @@ import { roles, roleSiteResources, sites, + Transaction, userSiteResources } from "@server/db"; import { siteResources, SiteResource } from "@server/db"; @@ -296,122 +297,16 @@ export async function updateSiteResource( ); } - const { mergedAllClients } = - await rebuildClientAssociationsFromSiteResource( - existingSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below - trx - ); - - // after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed - const destinationChanged = - existingSiteResource.destination !== - updatedSiteResource.destination; - const aliasChanged = - existingSiteResource.alias !== updatedSiteResource.alias; - - if (destinationChanged || aliasChanged) { - const [newt] = await trx - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - if (!newt) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Newt not found") - ); - } - - // Only update targets on newt if destination changed - if (destinationChanged) { - const oldTargets = generateSubnetProxyTargets( - existingSiteResource, - mergedAllClients - ); - const newTargets = generateSubnetProxyTargets( - updatedSiteResource, - mergedAllClients - ); - - await updateTargets(newt.newtId, { - oldTargets: oldTargets, - newTargets: newTargets - }); - } - - const olmJobs: Promise[] = []; - for (const client of mergedAllClients) { - // does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet - // todo: optimize this query if needed - const oldDestinationStillInUseSites = await trx - .select() - .from(siteResources) - .innerJoin( - clientSiteResourcesAssociationsCache, - eq( - clientSiteResourcesAssociationsCache.siteResourceId, - siteResources.siteResourceId - ) - ) - .where( - and( - eq( - clientSiteResourcesAssociationsCache.clientId, - client.clientId - ), - eq(siteResources.siteId, site.siteId), - eq( - siteResources.destination, - existingSiteResource.destination - ), - ne( - siteResources.siteResourceId, - existingSiteResource.siteResourceId - ) - ) - ); - - const oldDestinationStillInUseByASite = - oldDestinationStillInUseSites.length > 0; - - // we also need to update the remote subnets on the olms for each client that has access to this site - olmJobs.push( - updatePeerData( - client.clientId, - updatedSiteResource.siteId, - destinationChanged - ? { - oldRemoteSubnets: - !oldDestinationStillInUseByASite - ? generateRemoteSubnets([ - existingSiteResource - ]) - : [], - newRemoteSubnets: generateRemoteSubnets([ - updatedSiteResource - ]) - } - : undefined, - aliasChanged - ? { - oldAliases: generateAliasConfig([ - existingSiteResource - ]), - newAliases: generateAliasConfig([ - updatedSiteResource - ]) - } - : undefined - ) - ); - } - - await Promise.all(olmJobs); - } - logger.info( `Updated site resource ${siteResourceId} for site ${siteId}` ); + + await handleMessagingForUpdatedSiteResource( + existingSiteResource, + updatedSiteResource!, + { siteId: site.siteId, orgId: site.orgId }, + trx + ); }); return response(res, { @@ -431,3 +326,121 @@ export async function updateSiteResource( ); } } + +export async function handleMessagingForUpdatedSiteResource( + existingSiteResource: SiteResource, + updatedSiteResource: SiteResource, + site: { siteId: number; orgId: string }, + trx: Transaction +) { + const { mergedAllClients } = + await rebuildClientAssociationsFromSiteResource( + existingSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below + trx + ); + + // after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed + const destinationChanged = + existingSiteResource.destination !== updatedSiteResource.destination; + const aliasChanged = + existingSiteResource.alias !== updatedSiteResource.alias; + + if (destinationChanged || aliasChanged) { + const [newt] = await trx + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + throw new Error( + "Newt not found for site during site resource update" + ); + } + + // Only update targets on newt if destination changed + if (destinationChanged) { + const oldTargets = generateSubnetProxyTargets( + existingSiteResource, + mergedAllClients + ); + const newTargets = generateSubnetProxyTargets( + updatedSiteResource, + mergedAllClients + ); + + await updateTargets(newt.newtId, { + oldTargets: oldTargets, + newTargets: newTargets + }); + } + + const olmJobs: Promise[] = []; + for (const client of mergedAllClients) { + // does this client have access to another resource on this site that has the same destination still? if so we dont want to remove it from their olm yet + // todo: optimize this query if needed + const oldDestinationStillInUseSites = await trx + .select() + .from(siteResources) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq( + clientSiteResourcesAssociationsCache.siteResourceId, + siteResources.siteResourceId + ) + ) + .where( + and( + eq( + clientSiteResourcesAssociationsCache.clientId, + client.clientId + ), + eq(siteResources.siteId, site.siteId), + eq( + siteResources.destination, + existingSiteResource.destination + ), + ne( + siteResources.siteResourceId, + existingSiteResource.siteResourceId + ) + ) + ); + + const oldDestinationStillInUseByASite = + oldDestinationStillInUseSites.length > 0; + + // we also need to update the remote subnets on the olms for each client that has access to this site + olmJobs.push( + updatePeerData( + client.clientId, + updatedSiteResource.siteId, + destinationChanged + ? { + oldRemoteSubnets: !oldDestinationStillInUseByASite + ? generateRemoteSubnets([ + existingSiteResource + ]) + : [], + newRemoteSubnets: generateRemoteSubnets([ + updatedSiteResource + ]) + } + : undefined, + aliasChanged + ? { + oldAliases: generateAliasConfig([ + existingSiteResource + ]), + newAliases: generateAliasConfig([ + updatedSiteResource + ]) + } + : undefined + ) + ); + } + + await Promise.all(olmJobs); + } +}