diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 4ae0561a..a1cda2ec 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -654,7 +654,7 @@ export const clients = pgTable("clients", { maxConnections: integer("maxConnections") }); -export const clientSites = pgTable("clientSites", { +export const clientSitesAssociationsCache = pgTable("clientSitesAssociationsCache", { clientId: integer("clientId") .notNull() .references(() => clients.clientId, { onDelete: "cascade" }), @@ -665,6 +665,15 @@ export const clientSites = pgTable("clientSites", { endpoint: varchar("endpoint") }); +export const clientSiteResourcesAssociationsCache = pgTable("clientSiteResourcesAssociationsCache", { + clientId: integer("clientId") + .notNull() + .references(() => clients.clientId, { onDelete: "cascade" }), + siteResourceId: integer("siteResourceId") + .notNull() + .references(() => siteResources.siteResourceId, { onDelete: "cascade" }) +}); + export const olms = pgTable("olms", { olmId: varchar("id").primaryKey(), secretHash: varchar("secretHash").notNull(), @@ -847,7 +856,7 @@ export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; export type Client = InferSelectModel; -export type ClientSite = InferSelectModel; +export type ClientSite = InferSelectModel; export type Olm = InferSelectModel; export type OlmSession = InferSelectModel; export type UserClient = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 21929026..cae15a04 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -361,7 +361,7 @@ export const clients = sqliteTable("clients", { lastHolePunch: integer("lastHolePunch") }); -export const clientSites = sqliteTable("clientSites", { +export const clientSitesAssociationsCache = sqliteTable("clientSitesAssociationsCache", { clientId: integer("clientId") .notNull() .references(() => clients.clientId, { onDelete: "cascade" }), @@ -374,6 +374,15 @@ export const clientSites = sqliteTable("clientSites", { endpoint: text("endpoint") }); +export const clientSiteResourcesAssociationsCache = sqliteTable("clientSiteResourcesAssociationsCache", { + clientId: integer("clientId") + .notNull() + .references(() => clients.clientId, { onDelete: "cascade" }), + siteResourceId: integer("siteResourceId") + .notNull() + .references(() => siteResources.siteResourceId, { onDelete: "cascade" }) +}); + export const olms = sqliteTable("olms", { olmId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), @@ -894,7 +903,7 @@ export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; export type DnsRecord = InferSelectModel; export type Client = InferSelectModel; -export type ClientSite = InferSelectModel; +export type ClientSite = InferSelectModel; export type RoleClient = InferSelectModel; export type UserClient = InferSelectModel; export type SupporterKey = InferSelectModel; diff --git a/server/lib/calculateUserClientsForOrgs.ts b/server/lib/calculateUserClientsForOrgs.ts index f8578f0f..f66e3888 100644 --- a/server/lib/calculateUserClientsForOrgs.ts +++ b/server/lib/calculateUserClientsForOrgs.ts @@ -1,4 +1,4 @@ -import { clients, clientSites, db, olms, orgs, roleClients, roles, userClients, userOrgs, Transaction } from "@server/db"; +import { clients, clientSitesAssociationsCache, db, olms, orgs, roleClients, roles, userClients, userOrgs, Transaction } from "@server/db"; import { eq, and, notInArray } from "drizzle-orm"; import { listExitNodes } from "#dynamic/lib/exitNodes"; import { getNextAvailableClientSubnet } from "@server/lib/ip"; @@ -228,8 +228,8 @@ async function cleanupOrphanedClients( // Delete client-site associations first, then delete the clients for (const client of clientsToDelete) { await trx - .delete(clientSites) - .where(eq(clientSites.clientId, client.clientId)); + .delete(clientSitesAssociationsCache) + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); } if (clientsToDelete.length > 0) { diff --git a/server/lib/ip.ts b/server/lib/ip.ts index db8c6667..dd747797 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,9 +1,9 @@ -import { clientSites, db, SiteResource, Transaction } from "@server/db"; +import { clientSitesAssociationsCache, db, SiteResource, Transaction } from "@server/db"; import { clients, orgs, sites } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; import config from "@server/lib/config"; import z from "zod"; -import { getClientSiteResourceAccess } from "./rebuildSiteClientAssociations"; +import { getClientSiteResourceAccess } from "./rebuildClientAssociations"; import logger from "@server/logger"; interface IPRange { @@ -338,41 +338,54 @@ export type SubnetProxyTarget = { }[]; }; -export async function generateSubnetProxyTargets( - allSiteResources: SiteResource[], - trx: Transaction | typeof db = db -): Promise { +export function generateSingleSubnetProxyTargets( + siteResource: SiteResource, + clients: { + clientId: number; + pubKey: string | null; + subnet: string | null; + }[] +): SubnetProxyTarget[] { let targets: SubnetProxyTarget[] = []; - for (const siteResource of allSiteResources) { - const { mergedAllClients } = - await getClientSiteResourceAccess(siteResource, trx); + if (clients.length === 0) { + logger.debug( + `No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.` + ); + return []; + } - if (mergedAllClients.length === 0) { - logger.debug(`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`); + for (const clientSite of clients) { + if (!clientSite.subnet) { + logger.debug( + `Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.` + ); continue; } - for (const clientSite of mergedAllClients) { - const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; + const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; - if (siteResource.mode == "host") { - // check if this is a valid ip - const ipSchema = z.union([z.ipv4(), z.ipv6()]); - if (ipSchema.safeParse(siteResource.destination).success) { - targets.push({ - sourcePrefix: clientPrefix, - destPrefix: `${siteResource.destination}/32` - }); - } - } else if (siteResource.mode == "cidr") { + if (siteResource.mode == "host") { + // check if this is a valid ip + const ipSchema = z.union([z.ipv4(), z.ipv6()]); + if (ipSchema.safeParse(siteResource.destination).success) { targets.push({ sourcePrefix: clientPrefix, - destPrefix: siteResource.destination + destPrefix: `${siteResource.destination}/32` }); } + } else if (siteResource.mode == "cidr") { + targets.push({ + sourcePrefix: clientPrefix, + destPrefix: siteResource.destination + }); } } + // print a nice representation of the targets + logger.debug( + `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}` + ); + return targets; -} +} \ No newline at end of file diff --git a/server/lib/rebuildSiteClientAssociations.ts b/server/lib/rebuildClientAssociations.ts similarity index 70% rename from server/lib/rebuildSiteClientAssociations.ts rename to server/lib/rebuildClientAssociations.ts index 6e5c0629..8751cb1d 100644 --- a/server/lib/rebuildSiteClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -2,7 +2,7 @@ import { Client, clients, clientSiteResources, - clientSites, + clientSitesAssociationsCache, db, exitNodes, newts, @@ -29,7 +29,15 @@ import { } from "@server/routers/olm/peers"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; import logger from "@server/logger"; -import { generateRemoteSubnetsStr } from "@server/lib/ip"; +import { + generateRemoteSubnetsStr, + generateSingleSubnetProxyTargets, + SubnetProxyTarget +} from "@server/lib/ip"; +import { + addTargets as addSubnetProxyTargets, + removeTargets as removeSubnetProxyTargets +} from "@server/routers/client/targets"; export async function getClientSiteResourceAccess( siteResource: SiteResource, @@ -117,21 +125,29 @@ export async function getClientSiteResourceAccess( }; } -export async function rebuildSiteClientAssociations( +export async function rebuildClientAssociations( siteResource: SiteResource, trx: Transaction | typeof db = db -): Promise { +): Promise<{ + mergedAllClients: { + clientId: number; + pubKey: string | null; + subnet: string | null; + }[]; +}> { const siteId = siteResource.siteId; const { site, mergedAllClients, mergedAllClientIds } = await getClientSiteResourceAccess(siteResource, trx); + /////////// process the client-site associations /////////// + const existingClientSites = await trx .select({ - clientId: clientSites.clientId + clientId: clientSitesAssociationsCache.clientId }) - .from(clientSites) - .where(eq(clientSites.siteId, siteResource.siteId)); + .from(clientSitesAssociationsCache) + .where(eq(clientSitesAssociationsCache.siteId, siteResource.siteId)); const existingClientSiteIds = existingClientSites.map( (row) => row.clientId @@ -153,15 +169,16 @@ export async function rebuildSiteClientAssociations( (clientId) => !existingClientSiteIds.includes(clientId) ); - const clientSitesToInsert = mergedAllClientIds - .filter((clientId) => !existingClientSiteIds.includes(clientId)) - .map((clientId) => ({ - clientId, - siteId - })); + const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({ + clientId, + siteId + })); if (clientSitesToInsert.length > 0) { - await trx.insert(clientSites).values(clientSitesToInsert); + await trx + .insert(clientSitesAssociationsCache) + .values(clientSitesToInsert) + .returning(); } // Now remove any client-site associations that should no longer exist @@ -171,11 +188,68 @@ export async function rebuildSiteClientAssociations( if (clientSitesToRemove.length > 0) { await trx - .delete(clientSites) + .delete(clientSitesAssociationsCache) .where( and( - eq(clientSites.siteId, siteId), - inArray(clientSites.clientId, clientSitesToRemove) + eq(clientSitesAssociationsCache.siteId, siteId), + inArray( + clientSitesAssociationsCache.clientId, + clientSitesToRemove + ) + ) + ); + } + + /////////// process the client-siteResource associations /////////// + + const existingClientSiteResources = await trx + .select({ + clientId: clientSiteResources.clientId + }) + .from(clientSiteResources) + .where( + eq(clientSiteResources.siteResourceId, siteResource.siteResourceId) + ); + + const existingClientSiteResourceIds = existingClientSiteResources.map( + (row) => row.clientId + ); + + const clientSiteResourcesToAdd = mergedAllClientIds.filter( + (clientId) => !existingClientSiteResourceIds.includes(clientId) + ); + + const clientSiteResourcesToInsert = clientSiteResourcesToAdd.map( + (clientId) => ({ + clientId, + siteResourceId: siteResource.siteResourceId + }) + ); + + if (clientSiteResourcesToInsert.length > 0) { + await trx + .insert(clientSiteResources) + .values(clientSiteResourcesToInsert) + .returning(); + } + + const clientSiteResourcesToRemove = existingClientSiteResourceIds.filter( + (clientId) => !mergedAllClientIds.includes(clientId) + ); + + if (clientSiteResourcesToRemove.length > 0) { + await trx + .delete(clientSiteResources) + .where( + and( + eq( + clientSiteResources.siteResourceId, + siteResource.siteResourceId + ), + inArray( + clientSiteResources.clientId, + clientSiteResourcesToRemove + ) ) ); } @@ -190,6 +264,20 @@ export async function rebuildSiteClientAssociations( clientSitesToRemove, trx ); + + // Handle subnet proxy target updates for the resource associations + await handleSubnetProxyTargetUpdates( + siteResource, + mergedAllClients, + existingClients, + clientSiteResourcesToAdd, + clientSiteResourcesToRemove, + trx + ); + + return { + mergedAllClients + } } async function handleMessagesForSiteClients( @@ -401,9 +489,12 @@ export async function updateClientSiteDestinations( const sitesData = await trx .select() .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .innerJoin( + clientSitesAssociationsCache, + eq(sites.siteId, clientSitesAssociationsCache.siteId) + ) .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) - .where(eq(clientSites.clientId, client.clientId)); + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); for (const site of sitesData) { if (!site.sites.subnet) { @@ -411,7 +502,7 @@ export async function updateClientSiteDestinations( continue; } - if (!site.clientSites.endpoint) { + if (!site.clientSitesAssociationsCache.endpoint) { logger.warn(`Site ${site.sites.siteId} has no endpoint, skipping`); // if this is a new association the endpoint is not set yet // TODO: FIX THIS continue; } @@ -427,9 +518,9 @@ export async function updateClientSiteDestinations( exitNodeId: site.exitNodes?.exitNodeId || 0, type: site.exitNodes?.type || "", name: site.exitNodes?.name || "", - sourceIp: site.clientSites.endpoint.split(":")[0] || "", + sourceIp: site.clientSitesAssociationsCache.endpoint.split(":")[0] || "", sourcePort: - parseInt(site.clientSites.endpoint.split(":")[1]) || 0, + parseInt(site.clientSitesAssociationsCache.endpoint.split(":")[1]) || 0, destinations: [ { destinationIP: site.sites.subnet.split("/")[0], @@ -481,3 +572,76 @@ export async function updateClientSiteDestinations( }); } } + +async function handleSubnetProxyTargetUpdates( + siteResource: SiteResource, + allClients: { + clientId: number; + pubKey: string | null; + subnet: string | null; + }[], + existingClients: { + clientId: number; + pubKey: string | null; + subnet: string | null; + }[], + clientSiteResourcesToAdd: number[], + clientSiteResourcesToRemove: number[], + trx: Transaction | typeof db = db +): Promise { + // Get the newt for this site + const [newt] = await trx + .select() + .from(newts) + .where(eq(newts.siteId, siteResource.siteId)) + .limit(1); + + if (!newt) { + logger.warn( + `Newt not found for site ${siteResource.siteId}, skipping subnet proxy target updates` + ); + return; + } + + // Generate targets for added associations + if (clientSiteResourcesToAdd.length > 0) { + const addedClients = allClients.filter((client) => + clientSiteResourcesToAdd.includes(client.clientId) + ); + + if (addedClients.length > 0) { + const targetsToAdd = generateSingleSubnetProxyTargets( + siteResource, + addedClients + ); + + if (targetsToAdd.length > 0) { + logger.info( + `Adding ${targetsToAdd.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}` + ); + await addSubnetProxyTargets(newt.newtId, targetsToAdd); + } + } + } + + // Generate targets for removed associations + if (clientSiteResourcesToRemove.length > 0) { + const removedClients = existingClients.filter((client) => + clientSiteResourcesToRemove.includes(client.clientId) + ); + + if (removedClients.length > 0) { + const targetsToRemove = generateSingleSubnetProxyTargets( + siteResource, + removedClients + ); + + if (targetsToRemove.length > 0) { + logger.info( + `Removing ${targetsToRemove.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}` + ); + await removeSubnetProxyTargets(newt.newtId, targetsToRemove); + } + } + } +} diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 156f2e59..dc5e0081 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -345,9 +345,9 @@ export async function getTraefikConfig( routerMiddlewares.push(rewriteMiddlewareName); } - logger.debug( - `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` - ); + // logger.debug( + // `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` + // ); } catch (error) { logger.error( `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 5d6ea195..2d0e3ddb 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -434,9 +434,9 @@ export async function getTraefikConfig( routerMiddlewares.push(rewriteMiddlewareName); } - logger.debug( - `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` - ); + // logger.debug( + // `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` + // ); } catch (error) { logger.error( `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index 6beb52c6..70f1fe1a 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -69,9 +69,9 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) { ) ); - logger.debug( - `Cleaned up request audit logs older than ${retentionDays} days` - ); + // logger.debug( + // `Cleaned up request audit logs older than ${retentionDays} days` + // ); } catch (error) { logger.error("Error cleaning up old request audit logs:", error); } diff --git a/server/routers/client/deleteClient.ts b/server/routers/client/deleteClient.ts index 82ddf625..34019a53 100644 --- a/server/routers/client/deleteClient.ts +++ b/server/routers/client/deleteClient.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { clients, clientSites } from "@server/db"; +import { clients, clientSitesAssociationsCache } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -70,8 +70,8 @@ export async function deleteClient( await db.transaction(async (trx) => { // Delete the client-site associations first await trx - .delete(clientSites) - .where(eq(clientSites.clientId, clientId)); + .delete(clientSitesAssociationsCache) + .where(eq(clientSitesAssociationsCache.clientId, clientId)); // Then delete the client itself await trx.delete(clients).where(eq(clients.clientId, clientId)); diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index a8730faf..c9d22b10 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { clients, clientSites } from "@server/db"; +import { clients, clientSitesAssociationsCache } from "@server/db"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -29,9 +29,9 @@ async function query(clientId: number) { // Get the siteIds associated with this client const sites = await db - .select({ siteId: clientSites.siteId }) - .from(clientSites) - .where(eq(clientSites.clientId, clientId)); + .select({ siteId: clientSitesAssociationsCache.siteId }) + .from(clientSitesAssociationsCache) + .where(eq(clientSitesAssociationsCache.clientId, clientId)); // Add the siteIds to the client object return { diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 886cca7e..ff1050ef 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -5,7 +5,7 @@ import { roleClients, sites, userClients, - clientSites + clientSitesAssociationsCache } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; @@ -142,14 +142,14 @@ async function getSiteAssociations(clientIds: number[]) { return db .select({ - clientId: clientSites.clientId, - siteId: clientSites.siteId, + clientId: clientSitesAssociationsCache.clientId, + siteId: clientSitesAssociationsCache.siteId, siteName: sites.name, siteNiceId: sites.niceId }) - .from(clientSites) - .leftJoin(sites, eq(clientSites.siteId, sites.siteId)) - .where(inArray(clientSites.clientId, clientIds)); + .from(clientSitesAssociationsCache) + .leftJoin(sites, eq(clientSitesAssociationsCache.siteId, sites.siteId)) + .where(inArray(clientSitesAssociationsCache.clientId, clientIds)); } type OlmWithUpdateAvailable = Awaited>[0] & { diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index f0f70fd3..c3e2613c 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -3,7 +3,7 @@ import { SubnetProxyTarget } from "@server/lib/ip"; export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { await sendToClient(newtId, { - type: `newt/wg/target/add`, + type: `newt/wg/targets/add`, data: targets }); } @@ -13,7 +13,7 @@ export async function removeTargets( targets: SubnetProxyTarget[] ) { await sendToClient(newtId, { - type: `newt/wg/target/remove`, + type: `newt/wg/targets/remove`, data: targets }); } @@ -26,7 +26,7 @@ export async function updateTargets( } ) { await sendToClient(newtId, { - type: `newt/wg/target/update`, + type: `newt/wg/targets/update`, data: targets }); } diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 490674f5..e2f11a7c 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { Client, db, exitNodes, olms, sites } from "@server/db"; -import { clients, clientSites } from "@server/db"; +import { clients, clientSitesAssociationsCache } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; diff --git a/server/routers/gerbil/getAllRelays.ts b/server/routers/gerbil/getAllRelays.ts index 6eaf87e2..b7d33b95 100644 --- a/server/routers/gerbil/getAllRelays.ts +++ b/server/routers/gerbil/getAllRelays.ts @@ -7,7 +7,7 @@ import { olms, Site, sites, - clientSites, + clientSitesAssociationsCache, ExitNode } from "@server/db"; import { db } from "@server/db"; @@ -109,8 +109,8 @@ export async function generateRelayMappings(exitNode: ExitNode) { // Find all clients associated with this site through clientSites const clientSitesRes = await db .select() - .from(clientSites) - .where(eq(clientSites.siteId, site.siteId)); + .from(clientSitesAssociationsCache) + .where(eq(clientSitesAssociationsCache.siteId, site.siteId)); for (const clientSite of clientSitesRes) { if (!clientSite.endpoint) { diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 34bc2c6b..031cd23e 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -6,7 +6,7 @@ import { olms, Site, sites, - clientSites, + clientSitesAssociationsCache, exitNodes, ExitNode } from "@server/db"; @@ -174,11 +174,11 @@ export async function updateAndGenerateEndpointDestinations( listenPort: sites.listenPort }) .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) + .innerJoin(clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId)) .where( and( eq(sites.exitNodeId, exitNode.exitNodeId), - eq(clientSites.clientId, olm.clientId) + eq(clientSitesAssociationsCache.clientId, olm.clientId) ) ); @@ -189,14 +189,14 @@ export async function updateAndGenerateEndpointDestinations( ); await db - .update(clientSites) + .update(clientSitesAssociationsCache) .set({ endpoint: `${ip}:${port}` }) .where( and( - eq(clientSites.clientId, olm.clientId), - eq(clientSites.siteId, site.siteId) + eq(clientSitesAssociationsCache.clientId, olm.clientId), + eq(clientSitesAssociationsCache.siteId, site.siteId) ) ); } diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 83a208c5..1d132bc2 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -11,7 +11,7 @@ import { Target, targets } from "@server/db"; -import { clients, clientSites, Newt, sites } from "@server/db"; +import { clients, clientSitesAssociationsCache, Newt, sites } from "@server/db"; import { eq, and, inArray } from "drizzle-orm"; import { updatePeer } from "../olm/peers"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; @@ -138,8 +138,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { const clientsRes = await db .select() .from(clients) - .innerJoin(clientSites, eq(clients.clientId, clientSites.clientId)) - .where(eq(clientSites.siteId, siteId)); + .innerJoin(clientSitesAssociationsCache, eq(clients.clientId, clientSitesAssociationsCache.clientId)) + .where(eq(clientSitesAssociationsCache.siteId, siteId)); // Prepare peers data for the response const peers = await Promise.all( diff --git a/server/routers/olm/deleteUserOlm.ts b/server/routers/olm/deleteUserOlm.ts index d0c1e246..e1a23a43 100644 --- a/server/routers/olm/deleteUserOlm.ts +++ b/server/routers/olm/deleteUserOlm.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from "express"; import { db } from "@server/db"; -import { olms, clients, clientSites } from "@server/db"; +import { olms, clients, clientSitesAssociationsCache } from "@server/db"; import { eq } from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -57,8 +57,8 @@ export async function deleteUserOlm( // Delete client-site associations for each associated client for (const client of associatedClients) { await trx - .delete(clientSites) - .where(eq(clientSites.clientId, client.clientId)); + .delete(clientSitesAssociationsCache) + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); } // Delete all associated clients diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index dee52417..734c29f3 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -12,7 +12,7 @@ import { users } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; -import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db"; +import { clients, clientSitesAssociationsCache, exitNodes, Olm, olms, sites } from "@server/db"; import { and, eq, inArray, isNull } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; @@ -159,19 +159,19 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // set isRelay to false for all of the client's sites to reset the connection metadata await db - .update(clientSites) + .update(clientSitesAssociationsCache) .set({ isRelayed: relay == true }) - .where(eq(clientSites.clientId, client.clientId)); + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); } // Get all sites data const sitesData = await db .select() .from(sites) - .innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) - .where(eq(clientSites.clientId, client.clientId)); + .innerJoin(clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId)) + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); // Prepare an array to store site configurations const siteConfigurations = []; @@ -225,11 +225,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { const [clientSite] = await db .select() - .from(clientSites) + .from(clientSitesAssociationsCache) .where( and( - eq(clientSites.clientId, client.clientId), - eq(clientSites.siteId, site.siteId) + eq(clientSitesAssociationsCache.clientId, client.clientId), + eq(clientSitesAssociationsCache.siteId, site.siteId) ) ) .limit(1); diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index 9b31754c..153c4e7c 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -1,6 +1,6 @@ import { db, exitNodes, sites } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; -import { clients, clientSites, Olm } from "@server/db"; +import { clients, clientSitesAssociationsCache, Olm } from "@server/db"; import { and, eq } from "drizzle-orm"; import { updatePeer } from "../newt/peers"; import logger from "@server/logger"; @@ -67,14 +67,14 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { } await db - .update(clientSites) + .update(clientSitesAssociationsCache) .set({ isRelayed: true }) .where( and( - eq(clientSites.clientId, olm.clientId), - eq(clientSites.siteId, siteId) + eq(clientSitesAssociationsCache.clientId, olm.clientId), + eq(clientSitesAssociationsCache.siteId, siteId) ) ); diff --git a/server/routers/siteResource/addClientToSiteResource.ts b/server/routers/siteResource/addClientToSiteResource.ts index 2938ec78..8fb6afdc 100644 --- a/server/routers/siteResource/addClientToSiteResource.ts +++ b/server/routers/siteResource/addClientToSiteResource.ts @@ -8,7 +8,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const addClientToSiteResourceBodySchema = z .object({ @@ -136,7 +136,7 @@ export async function addClientToSiteResource( siteResourceId }); - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/addRoleToSiteResource.ts b/server/routers/siteResource/addRoleToSiteResource.ts index 2a5c1a7e..859ca5be 100644 --- a/server/routers/siteResource/addRoleToSiteResource.ts +++ b/server/routers/siteResource/addRoleToSiteResource.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const addRoleToSiteResourceBodySchema = z .object({ @@ -146,7 +146,7 @@ export async function addRoleToSiteResource( siteResourceId }); - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/addUserToSiteResource.ts b/server/routers/siteResource/addUserToSiteResource.ts index 279f5350..411d37b4 100644 --- a/server/routers/siteResource/addUserToSiteResource.ts +++ b/server/routers/siteResource/addUserToSiteResource.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const addUserToSiteResourceBodySchema = z .object({ @@ -115,7 +115,7 @@ export async function addUserToSiteResource( siteResourceId }); - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 478615ac..618256be 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -1,7 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, newts, roleResources, roles, roleSiteResources } from "@server/db"; -import { siteResources, sites, orgs, SiteResource } from "@server/db"; +import { + clientSiteResources, + db, + newts, + roles, + roleSiteResources, + userSiteResources +} from "@server/db"; +import { siteResources, sites, SiteResource } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -9,10 +16,8 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -import { addTargets } from "../client/targets"; import { getUniqueSiteResourceName } from "@server/db/names"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; -import { generateSubnetProxyTargets } from "@server/lib/ip"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const createSiteResourceParamsSchema = z.strictObject({ siteId: z.string().transform(Number).pipe(z.int().positive()), @@ -23,12 +28,15 @@ const createSiteResourceSchema = z .strictObject({ name: z.string().min(1).max(255), mode: z.enum(["host", "cidr", "port"]), - protocol: z.enum(["tcp", "udp"]).optional(), + // 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().optional() + alias: z.string().optional(), + userIds: z.array(z.string()), + roleIds: z.array(z.int()), + clientIds: z.array(z.int()) }) .strict() // .refine( @@ -138,12 +146,15 @@ export async function createSiteResource( const { name, mode, - protocol, + // protocol, // proxyPort, // destinationPort, destination, enabled, - alias + alias, + userIds, + roleIds, + clientIds } = parsedBody.data; // Verify the site exists and belongs to the org @@ -194,7 +205,7 @@ export async function createSiteResource( orgId, name, mode, - protocol: mode === "port" ? protocol : null, + // protocol: mode === "port" ? protocol : null, // proxyPort: mode === "port" ? proxyPort : null, // destinationPort: mode === "port" ? destinationPort : null, destination, @@ -203,6 +214,10 @@ export async function createSiteResource( }) .returning(); + const siteResourceId = newSiteResource.siteResourceId; + + //////////////////// update the associations //////////////////// + const [adminRole] = await trx .select() .from(roles) @@ -217,9 +232,34 @@ export async function createSiteResource( await trx.insert(roleSiteResources).values({ roleId: adminRole.roleId, - siteResourceId: newSiteResource.siteResourceId + 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 + })) + ); + } + const [newt] = await trx .select() .from(newts) @@ -232,10 +272,10 @@ export async function createSiteResource( ); } - const targets = await generateSubnetProxyTargets([newSiteResource], trx); - await addTargets(newt.newtId, targets); + // const targets = await generateSubnetProxyTargets([newSiteResource], trx); + // await addTargets(newt.newtId, targets); - await rebuildSiteClientAssociations(newSiteResource, trx); // we need to call this because we added to the admin role + await rebuildClientAssociations(newSiteResource, trx); // we need to call this because we added to the admin role }); if (!newSiteResource) { diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index 7d9f53ce..d2838a5a 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -9,9 +9,7 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -import { removeTargets } from "../client/targets"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; -import { generateSubnetProxyTargets } from "@server/lib/ip"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const deleteSiteResourceParamsSchema = z.strictObject({ siteResourceId: z.string().transform(Number).pipe(z.int().positive()), @@ -108,10 +106,10 @@ export async function deleteSiteResource( ); } - const targets = await generateSubnetProxyTargets([removedSiteResource], trx); - await removeTargets(newt.newtId, targets); + // const targets = await generateSubnetProxyTargets([removedSiteResource], trx); + // await removeTargets(newt.newtId, targets); - await rebuildSiteClientAssociations(existingSiteResource, trx); + await rebuildClientAssociations(existingSiteResource, trx); }); logger.info( diff --git a/server/routers/siteResource/removeClientFromSiteResource.ts b/server/routers/siteResource/removeClientFromSiteResource.ts index c7eae230..d46e5d67 100644 --- a/server/routers/siteResource/removeClientFromSiteResource.ts +++ b/server/routers/siteResource/removeClientFromSiteResource.ts @@ -8,7 +8,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const removeClientFromSiteResourceBodySchema = z .object({ @@ -142,7 +142,7 @@ export async function removeClientFromSiteResource( ) ); - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/removeRoleFromSiteResource.ts b/server/routers/siteResource/removeRoleFromSiteResource.ts index 2d8f1221..c4c68e06 100644 --- a/server/routers/siteResource/removeRoleFromSiteResource.ts +++ b/server/routers/siteResource/removeRoleFromSiteResource.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const removeRoleFromSiteResourceBodySchema = z .object({ @@ -151,7 +151,7 @@ export async function removeRoleFromSiteResource( ) ); - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/removeUserFromSiteResource.ts b/server/routers/siteResource/removeUserFromSiteResource.ts index 5463e6a1..8a90b752 100644 --- a/server/routers/siteResource/removeUserFromSiteResource.ts +++ b/server/routers/siteResource/removeUserFromSiteResource.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const removeUserFromSiteResourceBodySchema = z .object({ @@ -121,7 +121,7 @@ export async function removeUserFromSiteResource( ) ); - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/setSiteResourceClients.ts b/server/routers/siteResource/setSiteResourceClients.ts index aa44d658..974b27cc 100644 --- a/server/routers/siteResource/setSiteResourceClients.ts +++ b/server/routers/siteResource/setSiteResourceClients.ts @@ -8,7 +8,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, inArray } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const setSiteResourceClientsBodySchema = z .object({ @@ -119,17 +119,12 @@ export async function setSiteResourceClients( .where(eq(clientSiteResources.siteResourceId, siteResourceId)); if (clientIds.length > 0) { - await Promise.all( - clientIds.map((clientId) => - trx - .insert(clientSiteResources) - .values({ clientId, siteResourceId }) - .returning() - ) - ); + await trx + .insert(clientSiteResources) + .values(clientIds.map((clientId) => ({ clientId, siteResourceId }))); } - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/setSiteResourceRoles.ts b/server/routers/siteResource/setSiteResourceRoles.ts index 3b829d1f..df44e02b 100644 --- a/server/routers/siteResource/setSiteResourceRoles.ts +++ b/server/routers/siteResource/setSiteResourceRoles.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq, and, ne, inArray } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const setSiteResourceRolesBodySchema = z .object({ @@ -141,16 +141,13 @@ export async function setSiteResourceRoles( ); } - await Promise.all( - roleIds.map((roleId) => - trx - .insert(roleSiteResources) - .values({ roleId, siteResourceId }) - .returning() - ) - ); + if (roleIds.length > 0) { + await trx + .insert(roleSiteResources) + .values(roleIds.map((roleId) => ({ roleId, siteResourceId }))); + } - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/setSiteResourceUsers.ts b/server/routers/siteResource/setSiteResourceUsers.ts index ea913732..8ef9a0ab 100644 --- a/server/routers/siteResource/setSiteResourceUsers.ts +++ b/server/routers/siteResource/setSiteResourceUsers.ts @@ -9,7 +9,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; +import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations"; const setSiteResourceUsersBodySchema = z .object({ @@ -96,16 +96,13 @@ export async function setSiteResourceUsers( .delete(userSiteResources) .where(eq(userSiteResources.siteResourceId, siteResourceId)); - await Promise.all( - userIds.map((userId) => - trx - .insert(userSiteResources) - .values({ userId, siteResourceId }) - .returning() - ) - ); + if (userIds.length > 0) { + await trx + .insert(userSiteResources) + .values(userIds.map((userId) => ({ userId, siteResourceId }))); + } - await rebuildSiteClientAssociations(siteResource, trx); + await rebuildClientAssociations(siteResource, trx); }); return response(res, { diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 754909ef..61da94d2 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -1,16 +1,28 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, newts, sites } from "@server/db"; +import { + clientSiteResources, + db, + newts, + roles, + roleSiteResources, + sites, + userSiteResources +} from "@server/db"; import { siteResources, SiteResource } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; +import { eq, and, ne } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import { updateTargets } from "@server/routers/client/targets"; -import { generateSubnetProxyTargets } from "@server/lib/ip"; +import { generateSingleSubnetProxyTargets } from "@server/lib/ip"; +import { + getClientSiteResourceAccess, + rebuildClientAssociations +} from "@server/lib/rebuildClientAssociations"; const updateSiteResourceParamsSchema = z.strictObject({ siteResourceId: z.string().transform(Number).pipe(z.int().positive()), @@ -23,12 +35,15 @@ const updateSiteResourceSchema = z name: z.string().min(1).max(255).optional(), // mode: z.enum(["host", "cidr", "port"]).optional(), mode: z.enum(["host", "cidr"]).optional(), - protocol: z.enum(["tcp", "udp"]).nullish(), + // protocol: z.enum(["tcp", "udp"]).nullish(), // proxyPort: z.int().positive().nullish(), // destinationPort: z.int().positive().nullish(), destination: z.string().min(1).optional(), enabled: z.boolean().optional(), - alias: z.string().nullish() + alias: z.string().nullish(), + userIds: z.array(z.string()), + roleIds: z.array(z.int()), + clientIds: z.array(z.int()) }) .strict(); @@ -82,7 +97,16 @@ export async function updateSiteResource( } const { siteResourceId, siteId, orgId } = parsedParams.data; - const updateData = parsedBody.data; + const { + name, + mode, + destination, + alias, + enabled, + userIds, + roleIds, + clientIds + } = parsedBody.data; const [site] = await db .select() @@ -113,85 +137,131 @@ export async function updateSiteResource( ); } - // Determine the final mode and validate port mode requirements - const finalMode = updateData.mode || existingSiteResource.mode; - const finalProtocol = updateData.protocol !== undefined ? updateData.protocol : existingSiteResource.protocol; - // const finalProxyPort = updateData.proxyPort !== undefined ? updateData.proxyPort : existingSiteResource.proxyPort; - // const finalDestinationPort = updateData.destinationPort !== undefined ? updateData.destinationPort : existingSiteResource.destinationPort; - - // Prepare update data - const updateValues: any = {}; - if (updateData.name !== undefined) updateValues.name = updateData.name; - if (updateData.mode !== undefined) updateValues.mode = updateData.mode; - if (updateData.destination !== undefined) - updateValues.destination = updateData.destination; - if (updateData.enabled !== undefined) - updateValues.enabled = updateData.enabled; - - // Handle nullish fields (can be undefined, null, or a value) - if (updateData.alias !== undefined) { - updateValues.alias = - updateData.alias && updateData.alias.trim() - ? updateData.alias - : null; - } - - // Handle port mode fields - include in update if explicitly provided (null or value) or if mode changed - // const isModeChangingFromPort = - // existingSiteResource.mode === "port" && - // updateData.mode && - // updateData.mode !== "port"; - - // if (updateData.protocol !== undefined || isModeChangingFromPort) { - // updateValues.protocol = finalMode === "port" ? finalProtocol : null; - // } - // if (updateData.proxyPort !== undefined || isModeChangingFromPort) { - // updateValues.proxyPort = - // finalMode === "port" ? finalProxyPort : null; - // } - // if ( - // updateData.destinationPort !== undefined || - // isModeChangingFromPort - // ) { - // updateValues.destinationPort = - // finalMode === "port" ? finalDestinationPort : null; - // } - - // Update the site resource - const [updatedSiteResource] = await db - .update(siteResources) - .set(updateValues) - .where( - and( - eq(siteResources.siteResourceId, siteResourceId), - eq(siteResources.siteId, siteId), - eq(siteResources.orgId, orgId) + let updatedSiteResource: SiteResource | undefined; + await db.transaction(async (trx) => { + // Update the site resource + [updatedSiteResource] = await trx + .update(siteResources) + .set({ + name: name, + mode: mode, + destination: destination, + enabled: enabled, + alias: alias && alias.trim() ? alias : null + }) + .where( + and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) ) - ) - .returning(); + .returning(); - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); + //////////////////// update the associations //////////////////// - if (!newt) { - return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); - } + await trx + .delete(clientSiteResources) + .where(eq(clientSiteResources.siteResourceId, siteResourceId)); - const oldTargets = await generateSubnetProxyTargets([existingSiteResource]); - const newTargets = await generateSubnetProxyTargets([updatedSiteResource]); + if (clientIds.length > 0) { + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } - await updateTargets(newt.newtId, { - oldTargets: oldTargets, - newTargets: newTargets + 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 })) + ); + } + + const { mergedAllClients } = await rebuildClientAssociations( + updatedSiteResource, + trx + ); // we need to call this because we added to the admin role + + // after everything is rebuilt above we still need to update the targets if the destination changed + if ( + existingSiteResource.destination !== + updatedSiteResource.destination + ) { + 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") + ); + } + + const oldTargets = generateSingleSubnetProxyTargets( + existingSiteResource, + mergedAllClients + ); + const newTargets = generateSingleSubnetProxyTargets( + updatedSiteResource, + mergedAllClients + ); + + await updateTargets(newt.newtId, { + oldTargets: oldTargets, + newTargets: newTargets + }); + } + + logger.info( + `Updated site resource ${siteResourceId} for site ${siteId}` + ); }); - logger.info( - `Updated site resource ${siteResourceId} for site ${siteId}` - ); - return response(res, { data: updatedSiteResource, success: true, diff --git a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx index 4dd1068a..3ad25ca1 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx @@ -37,13 +37,7 @@ import { ListSitesResponse } from "@server/routers/site"; import { useTranslations } from "next-intl"; const GeneralFormSchema = z.object({ - name: z.string().nonempty("Name is required"), - siteIds: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) + name: z.string().nonempty("Name is required") }); type GeneralFormValues = z.infer; @@ -54,15 +48,11 @@ export default function GeneralPage() { const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); const router = useRouter(); - const [sites, setSites] = useState([]); - const [clientSites, setClientSites] = useState([]); - const [activeSitesTagIndex, setActiveSitesTagIndex] = useState(null); const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { - name: client?.name, - siteIds: [] + name: client?.name }, mode: "onChange" }); @@ -75,23 +65,6 @@ export default function GeneralPage() { const res = await api.get>( `/org/${client?.orgId}/sites/` ); - - const availableSites = res.data.data.sites - .filter((s) => s.type === "newt" && s.subnet) - .map((site) => ({ - id: site.siteId.toString(), - text: site.name - })); - - setSites(availableSites); - - // Filter sites to only include those assigned to the client - const assignedSites = availableSites.filter((site) => - client?.siteIds?.includes(parseInt(site.id)) - ); - setClientSites(assignedSites); - // Set the default values for the form - form.setValue("siteIds", assignedSites); } catch (e) { toast({ variant: "destructive", @@ -114,8 +87,7 @@ export default function GeneralPage() { try { await api.post(`/client/${client?.clientId}`, { - name: data.name, - siteIds: data.siteIds.map(site => parseInt(site.id)) + name: data.name }); updateClient({ name: data.name }); @@ -130,10 +102,7 @@ export default function GeneralPage() { toast({ variant: "destructive", title: t("clientUpdateFailed"), - description: formatAxiosError( - e, - t("clientUpdateError") - ) + description: formatAxiosError(e, t("clientUpdateError")) }); } finally { setLoading(false); diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index 6079eca2..b6c8b232 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -112,11 +112,11 @@ export default async function ResourcesPage(props: ResourcesPageProps) { siteName: siteResource.siteName, siteAddress: siteResource.siteAddress || null, mode: siteResource.mode || ("port" as any), - protocol: siteResource.protocol, - proxyPort: siteResource.proxyPort, + // protocol: siteResource.protocol, + // proxyPort: siteResource.proxyPort, siteId: siteResource.siteId, destination: siteResource.destination, - destinationPort: siteResource.destinationPort, + // destinationPort: siteResource.destinationPort, alias: siteResource.alias || null, siteNiceId: siteResource.siteNiceId }; diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 5a700d79..16179f8a 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -90,7 +90,7 @@ export default function CreateInternalResourceDialog({ mode: z.enum(["host", "cidr"]), destination: z.string().min(1), siteId: z.int().positive(t("createInternalResourceDialogPleaseSelectSite")), - protocol: z.enum(["tcp", "udp"]), + // protocol: z.enum(["tcp", "udp"]), // proxyPort: z.int() // .positive() // .min(1, t("createInternalResourceDialogProxyPortMin")) @@ -177,7 +177,7 @@ export default function CreateInternalResourceDialog({ name: "", siteId: availableSites[0]?.siteId || 0, mode: "host", - protocol: "tcp", + // protocol: "tcp", // proxyPort: undefined, destination: "", // destinationPort: undefined, @@ -196,7 +196,7 @@ export default function CreateInternalResourceDialog({ name: "", siteId: availableSites[0].siteId, mode: "host", - protocol: "tcp", + // protocol: "tcp", // proxyPort: undefined, destination: "", // destinationPort: undefined, @@ -260,35 +260,38 @@ export default function CreateInternalResourceDialog({ { name: data.name, mode: data.mode, - protocol: data.mode === "port" ? data.protocol : undefined, + // protocol: data.protocol, // proxyPort: data.mode === "port" ? data.proxyPort : undefined, // destinationPort: data.mode === "port" ? data.destinationPort : undefined, destination: data.destination, enabled: true, - alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined + alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined, + roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [], + userIds: data.users ? data.users.map((u) => u.id) : [], + clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : [] } ); const siteResourceId = response.data.data.siteResourceId; - // Set roles and users if provided - if (data.roles && data.roles.length > 0) { - await api.post(`/site-resource/${siteResourceId}/roles`, { - roleIds: data.roles.map((r) => parseInt(r.id)) - }); - } + // // Set roles and users if provided + // if (data.roles && data.roles.length > 0) { + // await api.post(`/site-resource/${siteResourceId}/roles`, { + // roleIds: data.roles.map((r) => parseInt(r.id)) + // }); + // } - if (data.users && data.users.length > 0) { - await api.post(`/site-resource/${siteResourceId}/users`, { - userIds: data.users.map((u) => u.id) - }); - } + // if (data.users && data.users.length > 0) { + // await api.post(`/site-resource/${siteResourceId}/users`, { + // userIds: data.users.map((u) => u.id) + // }); + // } - if (data.clients && data.clients.length > 0) { - await api.post(`/site-resource/${siteResourceId}/clients`, { - clientIds: data.clients.map((c) => parseInt(c.id)) - }); - } + // if (data.clients && data.clients.length > 0) { + // await api.post(`/site-resource/${siteResourceId}/clients`, { + // clientIds: data.clients.map((c) => parseInt(c.id)) + // }); + // } toast({ title: t("createInternalResourceDialogSuccess"), @@ -444,7 +447,7 @@ export default function CreateInternalResourceDialog({ - {t("createInternalResourceDialogModePort")} + {/* {t("createInternalResourceDialogModePort")} */} {t("createInternalResourceDialogModeHost")} {t("createInternalResourceDialogModeCidr")} @@ -535,7 +538,7 @@ export default function CreateInternalResourceDialog({ {mode === "host" && t("createInternalResourceDialogDestinationHostDescription")} {mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")} - {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} + {/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */} diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 1508cc12..b575dc61 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -36,7 +36,6 @@ import { toast } from "@app/hooks/useToast"; import { useTranslations } from "next-intl"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { Separator } from "@app/components/ui/separator"; import { ListRolesResponse } from "@server/routers/role"; import { ListUsersResponse } from "@server/routers/user"; import { ListSiteResourceRolesResponse } from "@server/routers/siteResource/listSiteResourceRoles"; @@ -52,12 +51,13 @@ type InternalResourceData = { name: string; orgId: string; siteName: string; - mode: "host" | "cidr" | "port"; - protocol: string | null; - proxyPort: number | null; + // mode: "host" | "cidr" | "port"; + mode: "host" | "cidr"; + // protocol: string | null; + // proxyPort: number | null; siteId: number; destination: string; - destinationPort?: number | null; + // destinationPort?: number | null; alias?: string | null; }; @@ -83,10 +83,10 @@ export default function EditInternalResourceDialog({ const formSchema = z.object({ name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), mode: z.enum(["host", "cidr", "port"]), - protocol: z.enum(["tcp", "udp"]).nullish(), - proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(), + // protocol: z.enum(["tcp", "udp"]).nullish(), + // proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(), destination: z.string().min(1), - destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), + // destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), roles: z.array( z.object({ @@ -107,42 +107,42 @@ export default function EditInternalResourceDialog({ }) ).optional() }) - .refine( - (data) => { - if (data.mode === "port") { - return data.protocol !== undefined && data.protocol !== null; - } - return true; - }, - { - message: t("editInternalResourceDialogProtocol") + " is required for port mode", - path: ["protocol"] - } - ) - .refine( - (data) => { - if (data.mode === "port") { - return data.proxyPort !== undefined && data.proxyPort !== null; - } - return true; - }, - { - message: t("editInternalResourceDialogSitePort") + " is required for port mode", - path: ["proxyPort"] - } - ) - .refine( - (data) => { - if (data.mode === "port") { - return data.destinationPort !== undefined && data.destinationPort !== null; - } - return true; - }, - { - message: t("targetPort") + " is required for port mode", - path: ["destinationPort"] - } - ); + // .refine( + // (data) => { + // if (data.mode === "port") { + // return data.protocol !== undefined && data.protocol !== null; + // } + // return true; + // }, + // { + // message: t("editInternalResourceDialogProtocol") + " is required for port mode", + // path: ["protocol"] + // } + // ) + // .refine( + // (data) => { + // if (data.mode === "port") { + // return data.proxyPort !== undefined && data.proxyPort !== null; + // } + // return true; + // }, + // { + // message: t("editInternalResourceDialogSitePort") + " is required for port mode", + // path: ["proxyPort"] + // } + // ) + // .refine( + // (data) => { + // if (data.mode === "port") { + // return data.destinationPort !== undefined && data.destinationPort !== null; + // } + // return true; + // }, + // { + // message: t("targetPort") + " is required for port mode", + // path: ["destinationPort"] + // } + // ); type FormData = z.infer; @@ -160,10 +160,10 @@ export default function EditInternalResourceDialog({ defaultValues: { name: resource.name, mode: resource.mode || "host", - protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, - proxyPort: resource.proxyPort ?? undefined, + // protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, + // proxyPort: resource.proxyPort ?? undefined, destination: resource.destination || "", - destinationPort: resource.destinationPort ?? undefined, + // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, roles: [], users: [], @@ -277,10 +277,10 @@ export default function EditInternalResourceDialog({ form.reset({ name: resource.name, mode: resource.mode || "host", - protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, - proxyPort: resource.proxyPort ?? undefined, + // protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, + // proxyPort: resource.proxyPort ?? undefined, destination: resource.destination || "", - destinationPort: resource.destinationPort ?? undefined, + // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, roles: [], users: [], @@ -298,25 +298,28 @@ export default function EditInternalResourceDialog({ await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { name: data.name, mode: data.mode, - protocol: data.mode === "port" ? data.protocol : null, - proxyPort: data.mode === "port" ? data.proxyPort : null, - destinationPort: data.mode === "port" ? data.destinationPort : null, + // protocol: data.mode === "port" ? data.protocol : null, + // proxyPort: data.mode === "port" ? data.proxyPort : null, + // destinationPort: data.mode === "port" ? data.destinationPort : null, destination: data.destination, - alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : null + alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : null, + roleIds: (data.roles || []).map((r) => parseInt(r.id)), + userIds: (data.users || []).map((u) => u.id), + clientIds: (data.clients || []).map((c) => parseInt(c.id)) }); // Update roles, users, and clients - await Promise.all([ - api.post(`/site-resource/${resource.id}/roles`, { - roleIds: (data.roles || []).map((r) => parseInt(r.id)) - }), - api.post(`/site-resource/${resource.id}/users`, { - userIds: (data.users || []).map((u) => u.id) - }), - api.post(`/site-resource/${resource.id}/clients`, { - clientIds: (data.clients || []).map((c) => parseInt(c.id)) - }) - ]); + // await Promise.all([ + // api.post(`/site-resource/${resource.id}/roles`, { + // roleIds: (data.roles || []).map((r) => parseInt(r.id)) + // }), + // api.post(`/site-resource/${resource.id}/users`, { + // userIds: (data.users || []).map((u) => u.id) + // }), + // api.post(`/site-resource/${resource.id}/clients`, { + // clientIds: (data.clients || []).map((c) => parseInt(c.id)) + // }) + // ]); toast({ title: t("editInternalResourceDialogSuccess"), @@ -384,7 +387,7 @@ export default function EditInternalResourceDialog({ - {t("editInternalResourceDialogModePort")} + {/* {t("editInternalResourceDialogModePort")} */} {t("editInternalResourceDialogModeHost")} {t("editInternalResourceDialogModeCidr")} @@ -394,7 +397,7 @@ export default function EditInternalResourceDialog({ )} /> - {mode === "port" && ( + {/* {mode === "port" && (
- )} + )} */} @@ -459,14 +462,14 @@ export default function EditInternalResourceDialog({ {mode === "host" && t("editInternalResourceDialogDestinationHostDescription")} {mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")} - {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} + {/* {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} */} )} /> - {mode === "port" && ( + {/* {mode === "port" && ( )} /> - )} + )} */} diff --git a/src/components/ResourcesTable.tsx b/src/components/ResourcesTable.tsx index 6d4c1e47..4a28c586 100644 --- a/src/components/ResourcesTable.tsx +++ b/src/components/ResourcesTable.tsx @@ -19,7 +19,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, DropdownMenuLabel, - DropdownMenuSeparator, + DropdownMenuSeparator, DropdownMenuCheckboxItem } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; @@ -164,13 +164,14 @@ export type InternalResourceRow = { orgId: string; siteName: string; siteAddress: string | null; - mode: "host" | "cidr" | "port"; - protocol: string | null; - proxyPort: number | null; + // mode: "host" | "cidr" | "port"; + mode: "host" | "cidr"; + // protocol: string | null; + // proxyPort: number | null; siteId: number; siteNiceId: string; destination: string; - destinationPort: number | null; + // destinationPort: number | null; alias: string | null; }; @@ -515,10 +516,14 @@ export default function ResourcesTable({ > - {overallStatus === "online" && t("resourcesTableHealthy")} - {overallStatus === "degraded" && t("resourcesTableDegraded")} - {overallStatus === "offline" && t("resourcesTableOffline")} - {overallStatus === "unknown" && t("resourcesTableUnknown")} + {overallStatus === "online" && + t("resourcesTableHealthy")} + {overallStatus === "degraded" && + t("resourcesTableDegraded")} + {overallStatus === "offline" && + t("resourcesTableOffline")} + {overallStatus === "unknown" && + t("resourcesTableUnknown")} @@ -770,7 +775,11 @@ export default function ResourcesTable({
-