Build client site resource associations and send messages

This commit is contained in:
Owen
2025-11-19 18:05:42 -05:00
parent 806949879a
commit 937b36e756
36 changed files with 904 additions and 583 deletions

View File

@@ -654,7 +654,7 @@ export const clients = pgTable("clients", {
maxConnections: integer("maxConnections") maxConnections: integer("maxConnections")
}); });
export const clientSites = pgTable("clientSites", { export const clientSitesAssociationsCache = pgTable("clientSitesAssociationsCache", {
clientId: integer("clientId") clientId: integer("clientId")
.notNull() .notNull()
.references(() => clients.clientId, { onDelete: "cascade" }), .references(() => clients.clientId, { onDelete: "cascade" }),
@@ -665,6 +665,15 @@ export const clientSites = pgTable("clientSites", {
endpoint: varchar("endpoint") 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", { export const olms = pgTable("olms", {
olmId: varchar("id").primaryKey(), olmId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(), secretHash: varchar("secretHash").notNull(),
@@ -847,7 +856,7 @@ export type ApiKey = InferSelectModel<typeof apiKeys>;
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>; export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>; export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
export type Client = InferSelectModel<typeof clients>; export type Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSites>; export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
export type Olm = InferSelectModel<typeof olms>; export type Olm = InferSelectModel<typeof olms>;
export type OlmSession = InferSelectModel<typeof olmSessions>; export type OlmSession = InferSelectModel<typeof olmSessions>;
export type UserClient = InferSelectModel<typeof userClients>; export type UserClient = InferSelectModel<typeof userClients>;

View File

@@ -361,7 +361,7 @@ export const clients = sqliteTable("clients", {
lastHolePunch: integer("lastHolePunch") lastHolePunch: integer("lastHolePunch")
}); });
export const clientSites = sqliteTable("clientSites", { export const clientSitesAssociationsCache = sqliteTable("clientSitesAssociationsCache", {
clientId: integer("clientId") clientId: integer("clientId")
.notNull() .notNull()
.references(() => clients.clientId, { onDelete: "cascade" }), .references(() => clients.clientId, { onDelete: "cascade" }),
@@ -374,6 +374,15 @@ export const clientSites = sqliteTable("clientSites", {
endpoint: text("endpoint") 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", { export const olms = sqliteTable("olms", {
olmId: text("id").primaryKey(), olmId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(), secretHash: text("secretHash").notNull(),
@@ -894,7 +903,7 @@ export type ResourceRule = InferSelectModel<typeof resourceRules>;
export type Domain = InferSelectModel<typeof domains>; export type Domain = InferSelectModel<typeof domains>;
export type DnsRecord = InferSelectModel<typeof dnsRecords>; export type DnsRecord = InferSelectModel<typeof dnsRecords>;
export type Client = InferSelectModel<typeof clients>; export type Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSites>; export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
export type RoleClient = InferSelectModel<typeof roleClients>; export type RoleClient = InferSelectModel<typeof roleClients>;
export type UserClient = InferSelectModel<typeof userClients>; export type UserClient = InferSelectModel<typeof userClients>;
export type SupporterKey = InferSelectModel<typeof supporterKey>; export type SupporterKey = InferSelectModel<typeof supporterKey>;

View File

@@ -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 { eq, and, notInArray } from "drizzle-orm";
import { listExitNodes } from "#dynamic/lib/exitNodes"; import { listExitNodes } from "#dynamic/lib/exitNodes";
import { getNextAvailableClientSubnet } from "@server/lib/ip"; import { getNextAvailableClientSubnet } from "@server/lib/ip";
@@ -228,8 +228,8 @@ async function cleanupOrphanedClients(
// Delete client-site associations first, then delete the clients // Delete client-site associations first, then delete the clients
for (const client of clientsToDelete) { for (const client of clientsToDelete) {
await trx await trx
.delete(clientSites) .delete(clientSitesAssociationsCache)
.where(eq(clientSites.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
} }
if (clientsToDelete.length > 0) { if (clientsToDelete.length > 0) {

View File

@@ -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 { clients, orgs, sites } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm"; import { and, eq, isNotNull } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import z from "zod"; import z from "zod";
import { getClientSiteResourceAccess } from "./rebuildSiteClientAssociations"; import { getClientSiteResourceAccess } from "./rebuildClientAssociations";
import logger from "@server/logger"; import logger from "@server/logger";
interface IPRange { interface IPRange {
@@ -338,41 +338,54 @@ export type SubnetProxyTarget = {
}[]; }[];
}; };
export async function generateSubnetProxyTargets( export function generateSingleSubnetProxyTargets(
allSiteResources: SiteResource[], siteResource: SiteResource,
trx: Transaction | typeof db = db clients: {
): Promise<SubnetProxyTarget[]> { clientId: number;
pubKey: string | null;
subnet: string | null;
}[]
): SubnetProxyTarget[] {
let targets: SubnetProxyTarget[] = []; let targets: SubnetProxyTarget[] = [];
for (const siteResource of allSiteResources) { if (clients.length === 0) {
const { mergedAllClients } = logger.debug(
await getClientSiteResourceAccess(siteResource, trx); `No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
);
return [];
}
if (mergedAllClients.length === 0) { for (const clientSite of clients) {
logger.debug(`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`); if (!clientSite.subnet) {
logger.debug(
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
);
continue; continue;
} }
for (const clientSite of mergedAllClients) { const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
if (siteResource.mode == "host") { if (siteResource.mode == "host") {
// check if this is a valid ip // check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]); const ipSchema = z.union([z.ipv4(), z.ipv6()]);
if (ipSchema.safeParse(siteResource.destination).success) { if (ipSchema.safeParse(siteResource.destination).success) {
targets.push({
sourcePrefix: clientPrefix,
destPrefix: `${siteResource.destination}/32`
});
}
} else if (siteResource.mode == "cidr") {
targets.push({ targets.push({
sourcePrefix: clientPrefix, 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; return targets;
} }

View File

@@ -2,7 +2,7 @@ import {
Client, Client,
clients, clients,
clientSiteResources, clientSiteResources,
clientSites, clientSitesAssociationsCache,
db, db,
exitNodes, exitNodes,
newts, newts,
@@ -29,7 +29,15 @@ import {
} from "@server/routers/olm/peers"; } from "@server/routers/olm/peers";
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
import logger from "@server/logger"; 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( export async function getClientSiteResourceAccess(
siteResource: SiteResource, siteResource: SiteResource,
@@ -117,21 +125,29 @@ export async function getClientSiteResourceAccess(
}; };
} }
export async function rebuildSiteClientAssociations( export async function rebuildClientAssociations(
siteResource: SiteResource, siteResource: SiteResource,
trx: Transaction | typeof db = db trx: Transaction | typeof db = db
): Promise<void> { ): Promise<{
mergedAllClients: {
clientId: number;
pubKey: string | null;
subnet: string | null;
}[];
}> {
const siteId = siteResource.siteId; const siteId = siteResource.siteId;
const { site, mergedAllClients, mergedAllClientIds } = const { site, mergedAllClients, mergedAllClientIds } =
await getClientSiteResourceAccess(siteResource, trx); await getClientSiteResourceAccess(siteResource, trx);
/////////// process the client-site associations ///////////
const existingClientSites = await trx const existingClientSites = await trx
.select({ .select({
clientId: clientSites.clientId clientId: clientSitesAssociationsCache.clientId
}) })
.from(clientSites) .from(clientSitesAssociationsCache)
.where(eq(clientSites.siteId, siteResource.siteId)); .where(eq(clientSitesAssociationsCache.siteId, siteResource.siteId));
const existingClientSiteIds = existingClientSites.map( const existingClientSiteIds = existingClientSites.map(
(row) => row.clientId (row) => row.clientId
@@ -153,15 +169,16 @@ export async function rebuildSiteClientAssociations(
(clientId) => !existingClientSiteIds.includes(clientId) (clientId) => !existingClientSiteIds.includes(clientId)
); );
const clientSitesToInsert = mergedAllClientIds const clientSitesToInsert = clientSitesToAdd.map((clientId) => ({
.filter((clientId) => !existingClientSiteIds.includes(clientId)) clientId,
.map((clientId) => ({ siteId
clientId, }));
siteId
}));
if (clientSitesToInsert.length > 0) { 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 // Now remove any client-site associations that should no longer exist
@@ -171,11 +188,68 @@ export async function rebuildSiteClientAssociations(
if (clientSitesToRemove.length > 0) { if (clientSitesToRemove.length > 0) {
await trx await trx
.delete(clientSites) .delete(clientSitesAssociationsCache)
.where( .where(
and( and(
eq(clientSites.siteId, siteId), eq(clientSitesAssociationsCache.siteId, siteId),
inArray(clientSites.clientId, clientSitesToRemove) 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, clientSitesToRemove,
trx trx
); );
// Handle subnet proxy target updates for the resource associations
await handleSubnetProxyTargetUpdates(
siteResource,
mergedAllClients,
existingClients,
clientSiteResourcesToAdd,
clientSiteResourcesToRemove,
trx
);
return {
mergedAllClients
}
} }
async function handleMessagesForSiteClients( async function handleMessagesForSiteClients(
@@ -401,9 +489,12 @@ export async function updateClientSiteDestinations(
const sitesData = await trx const sitesData = await trx
.select() .select()
.from(sites) .from(sites)
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) .innerJoin(
clientSitesAssociationsCache,
eq(sites.siteId, clientSitesAssociationsCache.siteId)
)
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
.where(eq(clientSites.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
for (const site of sitesData) { for (const site of sitesData) {
if (!site.sites.subnet) { if (!site.sites.subnet) {
@@ -411,7 +502,7 @@ export async function updateClientSiteDestinations(
continue; 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 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; continue;
} }
@@ -427,9 +518,9 @@ export async function updateClientSiteDestinations(
exitNodeId: site.exitNodes?.exitNodeId || 0, exitNodeId: site.exitNodes?.exitNodeId || 0,
type: site.exitNodes?.type || "", type: site.exitNodes?.type || "",
name: site.exitNodes?.name || "", name: site.exitNodes?.name || "",
sourceIp: site.clientSites.endpoint.split(":")[0] || "", sourceIp: site.clientSitesAssociationsCache.endpoint.split(":")[0] || "",
sourcePort: sourcePort:
parseInt(site.clientSites.endpoint.split(":")[1]) || 0, parseInt(site.clientSitesAssociationsCache.endpoint.split(":")[1]) || 0,
destinations: [ destinations: [
{ {
destinationIP: site.sites.subnet.split("/")[0], 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<void> {
// 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);
}
}
}
}

View File

@@ -345,9 +345,9 @@ export async function getTraefikConfig(
routerMiddlewares.push(rewriteMiddlewareName); routerMiddlewares.push(rewriteMiddlewareName);
} }
logger.debug( // logger.debug(
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` // `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
); // );
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`

View File

@@ -434,9 +434,9 @@ export async function getTraefikConfig(
routerMiddlewares.push(rewriteMiddlewareName); routerMiddlewares.push(rewriteMiddlewareName);
} }
logger.debug( // logger.debug(
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})` // `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
); // );
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}` `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`

View File

@@ -69,9 +69,9 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
) )
); );
logger.debug( // logger.debug(
`Cleaned up request audit logs older than ${retentionDays} days` // `Cleaned up request audit logs older than ${retentionDays} days`
); // );
} catch (error) { } catch (error) {
logger.error("Error cleaning up old request audit logs:", error); logger.error("Error cleaning up old request audit logs:", error);
} }

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { clients, clientSites } from "@server/db"; import { clients, clientSitesAssociationsCache } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -70,8 +70,8 @@ export async function deleteClient(
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// Delete the client-site associations first // Delete the client-site associations first
await trx await trx
.delete(clientSites) .delete(clientSitesAssociationsCache)
.where(eq(clientSites.clientId, clientId)); .where(eq(clientSitesAssociationsCache.clientId, clientId));
// Then delete the client itself // Then delete the client itself
await trx.delete(clients).where(eq(clients.clientId, clientId)); await trx.delete(clients).where(eq(clients.clientId, clientId));

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db } from "@server/db";
import { clients, clientSites } from "@server/db"; import { clients, clientSitesAssociationsCache } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -29,9 +29,9 @@ async function query(clientId: number) {
// Get the siteIds associated with this client // Get the siteIds associated with this client
const sites = await db const sites = await db
.select({ siteId: clientSites.siteId }) .select({ siteId: clientSitesAssociationsCache.siteId })
.from(clientSites) .from(clientSitesAssociationsCache)
.where(eq(clientSites.clientId, clientId)); .where(eq(clientSitesAssociationsCache.clientId, clientId));
// Add the siteIds to the client object // Add the siteIds to the client object
return { return {

View File

@@ -5,7 +5,7 @@ import {
roleClients, roleClients,
sites, sites,
userClients, userClients,
clientSites clientSitesAssociationsCache
} from "@server/db"; } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -142,14 +142,14 @@ async function getSiteAssociations(clientIds: number[]) {
return db return db
.select({ .select({
clientId: clientSites.clientId, clientId: clientSitesAssociationsCache.clientId,
siteId: clientSites.siteId, siteId: clientSitesAssociationsCache.siteId,
siteName: sites.name, siteName: sites.name,
siteNiceId: sites.niceId siteNiceId: sites.niceId
}) })
.from(clientSites) .from(clientSitesAssociationsCache)
.leftJoin(sites, eq(clientSites.siteId, sites.siteId)) .leftJoin(sites, eq(clientSitesAssociationsCache.siteId, sites.siteId))
.where(inArray(clientSites.clientId, clientIds)); .where(inArray(clientSitesAssociationsCache.clientId, clientIds));
} }
type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & { type OlmWithUpdateAvailable = Awaited<ReturnType<typeof queryClients>>[0] & {

View File

@@ -3,7 +3,7 @@ import { SubnetProxyTarget } from "@server/lib/ip";
export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) {
await sendToClient(newtId, { await sendToClient(newtId, {
type: `newt/wg/target/add`, type: `newt/wg/targets/add`,
data: targets data: targets
}); });
} }
@@ -13,7 +13,7 @@ export async function removeTargets(
targets: SubnetProxyTarget[] targets: SubnetProxyTarget[]
) { ) {
await sendToClient(newtId, { await sendToClient(newtId, {
type: `newt/wg/target/remove`, type: `newt/wg/targets/remove`,
data: targets data: targets
}); });
} }
@@ -26,7 +26,7 @@ export async function updateTargets(
} }
) { ) {
await sendToClient(newtId, { await sendToClient(newtId, {
type: `newt/wg/target/update`, type: `newt/wg/targets/update`,
data: targets data: targets
}); });
} }

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { Client, db, exitNodes, olms, sites } from "@server/db"; 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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";

View File

@@ -7,7 +7,7 @@ import {
olms, olms,
Site, Site,
sites, sites,
clientSites, clientSitesAssociationsCache,
ExitNode ExitNode
} from "@server/db"; } from "@server/db";
import { db } 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 // Find all clients associated with this site through clientSites
const clientSitesRes = await db const clientSitesRes = await db
.select() .select()
.from(clientSites) .from(clientSitesAssociationsCache)
.where(eq(clientSites.siteId, site.siteId)); .where(eq(clientSitesAssociationsCache.siteId, site.siteId));
for (const clientSite of clientSitesRes) { for (const clientSite of clientSitesRes) {
if (!clientSite.endpoint) { if (!clientSite.endpoint) {

View File

@@ -6,7 +6,7 @@ import {
olms, olms,
Site, Site,
sites, sites,
clientSites, clientSitesAssociationsCache,
exitNodes, exitNodes,
ExitNode ExitNode
} from "@server/db"; } from "@server/db";
@@ -174,11 +174,11 @@ export async function updateAndGenerateEndpointDestinations(
listenPort: sites.listenPort listenPort: sites.listenPort
}) })
.from(sites) .from(sites)
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) .innerJoin(clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId))
.where( .where(
and( and(
eq(sites.exitNodeId, exitNode.exitNodeId), 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 await db
.update(clientSites) .update(clientSitesAssociationsCache)
.set({ .set({
endpoint: `${ip}:${port}` endpoint: `${ip}:${port}`
}) })
.where( .where(
and( and(
eq(clientSites.clientId, olm.clientId), eq(clientSitesAssociationsCache.clientId, olm.clientId),
eq(clientSites.siteId, site.siteId) eq(clientSitesAssociationsCache.siteId, site.siteId)
) )
); );
} }

View File

@@ -11,7 +11,7 @@ import {
Target, Target,
targets targets
} from "@server/db"; } 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 { eq, and, inArray } from "drizzle-orm";
import { updatePeer } from "../olm/peers"; import { updatePeer } from "../olm/peers";
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
@@ -138,8 +138,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
const clientsRes = await db const clientsRes = await db
.select() .select()
.from(clients) .from(clients)
.innerJoin(clientSites, eq(clients.clientId, clientSites.clientId)) .innerJoin(clientSitesAssociationsCache, eq(clients.clientId, clientSitesAssociationsCache.clientId))
.where(eq(clientSites.siteId, siteId)); .where(eq(clientSitesAssociationsCache.siteId, siteId));
// Prepare peers data for the response // Prepare peers data for the response
const peers = await Promise.all( const peers = await Promise.all(

View File

@@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db } from "@server/db"; 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 { eq } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -57,8 +57,8 @@ export async function deleteUserOlm(
// Delete client-site associations for each associated client // Delete client-site associations for each associated client
for (const client of associatedClients) { for (const client of associatedClients) {
await trx await trx
.delete(clientSites) .delete(clientSitesAssociationsCache)
.where(eq(clientSites.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
} }
// Delete all associated clients // Delete all associated clients

View File

@@ -12,7 +12,7 @@ import {
users users
} from "@server/db"; } from "@server/db";
import { MessageHandler } from "@server/routers/ws"; 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 { and, eq, inArray, isNull } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers"; import { addPeer, deletePeer } from "../newt/peers";
import logger from "@server/logger"; 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 // set isRelay to false for all of the client's sites to reset the connection metadata
await db await db
.update(clientSites) .update(clientSitesAssociationsCache)
.set({ .set({
isRelayed: relay == true isRelayed: relay == true
}) })
.where(eq(clientSites.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
} }
// Get all sites data // Get all sites data
const sitesData = await db const sitesData = await db
.select() .select()
.from(sites) .from(sites)
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId)) .innerJoin(clientSitesAssociationsCache, eq(sites.siteId, clientSitesAssociationsCache.siteId))
.where(eq(clientSites.clientId, client.clientId)); .where(eq(clientSitesAssociationsCache.clientId, client.clientId));
// Prepare an array to store site configurations // Prepare an array to store site configurations
const siteConfigurations = []; const siteConfigurations = [];
@@ -225,11 +225,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
const [clientSite] = await db const [clientSite] = await db
.select() .select()
.from(clientSites) .from(clientSitesAssociationsCache)
.where( .where(
and( and(
eq(clientSites.clientId, client.clientId), eq(clientSitesAssociationsCache.clientId, client.clientId),
eq(clientSites.siteId, site.siteId) eq(clientSitesAssociationsCache.siteId, site.siteId)
) )
) )
.limit(1); .limit(1);

View File

@@ -1,6 +1,6 @@
import { db, exitNodes, sites } from "@server/db"; import { db, exitNodes, sites } from "@server/db";
import { MessageHandler } from "@server/routers/ws"; 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 { and, eq } from "drizzle-orm";
import { updatePeer } from "../newt/peers"; import { updatePeer } from "../newt/peers";
import logger from "@server/logger"; import logger from "@server/logger";
@@ -67,14 +67,14 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => {
} }
await db await db
.update(clientSites) .update(clientSitesAssociationsCache)
.set({ .set({
isRelayed: true isRelayed: true
}) })
.where( .where(
and( and(
eq(clientSites.clientId, olm.clientId), eq(clientSitesAssociationsCache.clientId, olm.clientId),
eq(clientSites.siteId, siteId) eq(clientSitesAssociationsCache.siteId, siteId)
) )
); );

View File

@@ -8,7 +8,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const addClientToSiteResourceBodySchema = z const addClientToSiteResourceBodySchema = z
.object({ .object({
@@ -136,7 +136,7 @@ export async function addClientToSiteResource(
siteResourceId siteResourceId
}); });
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const addRoleToSiteResourceBodySchema = z const addRoleToSiteResourceBodySchema = z
.object({ .object({
@@ -146,7 +146,7 @@ export async function addRoleToSiteResource(
siteResourceId siteResourceId
}); });
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const addUserToSiteResourceBodySchema = z const addUserToSiteResourceBodySchema = z
.object({ .object({
@@ -115,7 +115,7 @@ export async function addUserToSiteResource(
siteResourceId siteResourceId
}); });
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -1,7 +1,14 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, newts, roleResources, roles, roleSiteResources } from "@server/db"; import {
import { siteResources, sites, orgs, SiteResource } from "@server/db"; clientSiteResources,
db,
newts,
roles,
roleSiteResources,
userSiteResources
} from "@server/db";
import { siteResources, sites, SiteResource } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -9,10 +16,8 @@ import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { addTargets } from "../client/targets";
import { getUniqueSiteResourceName } from "@server/db/names"; import { getUniqueSiteResourceName } from "@server/db/names";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
import { generateSubnetProxyTargets } from "@server/lib/ip";
const createSiteResourceParamsSchema = z.strictObject({ const createSiteResourceParamsSchema = z.strictObject({
siteId: z.string().transform(Number).pipe(z.int().positive()), siteId: z.string().transform(Number).pipe(z.int().positive()),
@@ -23,12 +28,15 @@ const createSiteResourceSchema = z
.strictObject({ .strictObject({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
mode: z.enum(["host", "cidr", "port"]), mode: z.enum(["host", "cidr", "port"]),
protocol: z.enum(["tcp", "udp"]).optional(), // protocol: z.enum(["tcp", "udp"]).optional(),
// proxyPort: z.int().positive().optional(), // proxyPort: z.int().positive().optional(),
// destinationPort: z.int().positive().optional(), // destinationPort: z.int().positive().optional(),
destination: z.string().min(1), destination: z.string().min(1),
enabled: z.boolean().default(true), 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() .strict()
// .refine( // .refine(
@@ -138,12 +146,15 @@ export async function createSiteResource(
const { const {
name, name,
mode, mode,
protocol, // protocol,
// proxyPort, // proxyPort,
// destinationPort, // destinationPort,
destination, destination,
enabled, enabled,
alias alias,
userIds,
roleIds,
clientIds
} = parsedBody.data; } = parsedBody.data;
// Verify the site exists and belongs to the org // Verify the site exists and belongs to the org
@@ -194,7 +205,7 @@ export async function createSiteResource(
orgId, orgId,
name, name,
mode, mode,
protocol: mode === "port" ? protocol : null, // protocol: mode === "port" ? protocol : null,
// proxyPort: mode === "port" ? proxyPort : null, // proxyPort: mode === "port" ? proxyPort : null,
// destinationPort: mode === "port" ? destinationPort : null, // destinationPort: mode === "port" ? destinationPort : null,
destination, destination,
@@ -203,6 +214,10 @@ export async function createSiteResource(
}) })
.returning(); .returning();
const siteResourceId = newSiteResource.siteResourceId;
//////////////////// update the associations ////////////////////
const [adminRole] = await trx const [adminRole] = await trx
.select() .select()
.from(roles) .from(roles)
@@ -217,9 +232,34 @@ export async function createSiteResource(
await trx.insert(roleSiteResources).values({ await trx.insert(roleSiteResources).values({
roleId: adminRole.roleId, 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 const [newt] = await trx
.select() .select()
.from(newts) .from(newts)
@@ -232,10 +272,10 @@ export async function createSiteResource(
); );
} }
const targets = await generateSubnetProxyTargets([newSiteResource], trx); // const targets = await generateSubnetProxyTargets([newSiteResource], trx);
await addTargets(newt.newtId, targets); // 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) { if (!newSiteResource) {

View File

@@ -9,9 +9,7 @@ import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { removeTargets } from "../client/targets"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations";
import { generateSubnetProxyTargets } from "@server/lib/ip";
const deleteSiteResourceParamsSchema = z.strictObject({ const deleteSiteResourceParamsSchema = z.strictObject({
siteResourceId: z.string().transform(Number).pipe(z.int().positive()), siteResourceId: z.string().transform(Number).pipe(z.int().positive()),
@@ -108,10 +106,10 @@ export async function deleteSiteResource(
); );
} }
const targets = await generateSubnetProxyTargets([removedSiteResource], trx); // const targets = await generateSubnetProxyTargets([removedSiteResource], trx);
await removeTargets(newt.newtId, targets); // await removeTargets(newt.newtId, targets);
await rebuildSiteClientAssociations(existingSiteResource, trx); await rebuildClientAssociations(existingSiteResource, trx);
}); });
logger.info( logger.info(

View File

@@ -8,7 +8,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const removeClientFromSiteResourceBodySchema = z const removeClientFromSiteResourceBodySchema = z
.object({ .object({
@@ -142,7 +142,7 @@ export async function removeClientFromSiteResource(
) )
); );
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const removeRoleFromSiteResourceBodySchema = z const removeRoleFromSiteResourceBodySchema = z
.object({ .object({
@@ -151,7 +151,7 @@ export async function removeRoleFromSiteResource(
) )
); );
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const removeUserFromSiteResourceBodySchema = z const removeUserFromSiteResourceBodySchema = z
.object({ .object({
@@ -121,7 +121,7 @@ export async function removeUserFromSiteResource(
) )
); );
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -8,7 +8,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, inArray } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const setSiteResourceClientsBodySchema = z const setSiteResourceClientsBodySchema = z
.object({ .object({
@@ -119,17 +119,12 @@ export async function setSiteResourceClients(
.where(eq(clientSiteResources.siteResourceId, siteResourceId)); .where(eq(clientSiteResources.siteResourceId, siteResourceId));
if (clientIds.length > 0) { if (clientIds.length > 0) {
await Promise.all( await trx
clientIds.map((clientId) => .insert(clientSiteResources)
trx .values(clientIds.map((clientId) => ({ clientId, siteResourceId })));
.insert(clientSiteResources)
.values({ clientId, siteResourceId })
.returning()
)
);
} }
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq, and, ne, inArray } from "drizzle-orm"; import { eq, and, ne, inArray } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const setSiteResourceRolesBodySchema = z const setSiteResourceRolesBodySchema = z
.object({ .object({
@@ -141,16 +141,13 @@ export async function setSiteResourceRoles(
); );
} }
await Promise.all( if (roleIds.length > 0) {
roleIds.map((roleId) => await trx
trx .insert(roleSiteResources)
.insert(roleSiteResources) .values(roleIds.map((roleId) => ({ roleId, siteResourceId })));
.values({ roleId, siteResourceId }) }
.returning()
)
);
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -9,7 +9,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildSiteClientAssociations } from "@server/lib/rebuildSiteClientAssociations"; import { rebuildClientAssociations } from "@server/lib/rebuildClientAssociations";
const setSiteResourceUsersBodySchema = z const setSiteResourceUsersBodySchema = z
.object({ .object({
@@ -96,16 +96,13 @@ export async function setSiteResourceUsers(
.delete(userSiteResources) .delete(userSiteResources)
.where(eq(userSiteResources.siteResourceId, siteResourceId)); .where(eq(userSiteResources.siteResourceId, siteResourceId));
await Promise.all( if (userIds.length > 0) {
userIds.map((userId) => await trx
trx .insert(userSiteResources)
.insert(userSiteResources) .values(userIds.map((userId) => ({ userId, siteResourceId })));
.values({ userId, siteResourceId }) }
.returning()
)
);
await rebuildSiteClientAssociations(siteResource, trx); await rebuildClientAssociations(siteResource, trx);
}); });
return response(res, { return response(res, {

View File

@@ -1,16 +1,28 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; 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 { siteResources, SiteResource } from "@server/db";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; 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 { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { updateTargets } from "@server/routers/client/targets"; 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({ const updateSiteResourceParamsSchema = z.strictObject({
siteResourceId: z.string().transform(Number).pipe(z.int().positive()), siteResourceId: z.string().transform(Number).pipe(z.int().positive()),
@@ -23,12 +35,15 @@ const updateSiteResourceSchema = z
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
// mode: z.enum(["host", "cidr", "port"]).optional(), // mode: z.enum(["host", "cidr", "port"]).optional(),
mode: z.enum(["host", "cidr"]).optional(), mode: z.enum(["host", "cidr"]).optional(),
protocol: z.enum(["tcp", "udp"]).nullish(), // protocol: z.enum(["tcp", "udp"]).nullish(),
// proxyPort: z.int().positive().nullish(), // proxyPort: z.int().positive().nullish(),
// destinationPort: z.int().positive().nullish(), // destinationPort: z.int().positive().nullish(),
destination: z.string().min(1).optional(), destination: z.string().min(1).optional(),
enabled: z.boolean().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(); .strict();
@@ -82,7 +97,16 @@ export async function updateSiteResource(
} }
const { siteResourceId, siteId, orgId } = parsedParams.data; 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 const [site] = await db
.select() .select()
@@ -113,85 +137,131 @@ export async function updateSiteResource(
); );
} }
// Determine the final mode and validate port mode requirements let updatedSiteResource: SiteResource | undefined;
const finalMode = updateData.mode || existingSiteResource.mode; await db.transaction(async (trx) => {
const finalProtocol = updateData.protocol !== undefined ? updateData.protocol : existingSiteResource.protocol; // Update the site resource
// const finalProxyPort = updateData.proxyPort !== undefined ? updateData.proxyPort : existingSiteResource.proxyPort; [updatedSiteResource] = await trx
// const finalDestinationPort = updateData.destinationPort !== undefined ? updateData.destinationPort : existingSiteResource.destinationPort; .update(siteResources)
.set({
// Prepare update data name: name,
const updateValues: any = {}; mode: mode,
if (updateData.name !== undefined) updateValues.name = updateData.name; destination: destination,
if (updateData.mode !== undefined) updateValues.mode = updateData.mode; enabled: enabled,
if (updateData.destination !== undefined) alias: alias && alias.trim() ? alias : null
updateValues.destination = updateData.destination; })
if (updateData.enabled !== undefined) .where(
updateValues.enabled = updateData.enabled; and(
eq(siteResources.siteResourceId, siteResourceId),
// Handle nullish fields (can be undefined, null, or a value) eq(siteResources.siteId, siteId),
if (updateData.alias !== undefined) { eq(siteResources.orgId, orgId)
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)
) )
) .returning();
.returning();
const [newt] = await db //////////////////// update the associations ////////////////////
.select()
.from(newts)
.where(eq(newts.siteId, site.siteId))
.limit(1);
if (!newt) { await trx
return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); .delete(clientSiteResources)
} .where(eq(clientSiteResources.siteResourceId, siteResourceId));
const oldTargets = await generateSubnetProxyTargets([existingSiteResource]); if (clientIds.length > 0) {
const newTargets = await generateSubnetProxyTargets([updatedSiteResource]); await trx.insert(clientSiteResources).values(
clientIds.map((clientId) => ({
clientId,
siteResourceId
}))
);
}
await updateTargets(newt.newtId, { await trx
oldTargets: oldTargets, .delete(userSiteResources)
newTargets: newTargets .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, { return response(res, {
data: updatedSiteResource, data: updatedSiteResource,
success: true, success: true,

View File

@@ -37,13 +37,7 @@ import { ListSitesResponse } from "@server/routers/site";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const GeneralFormSchema = z.object({ const GeneralFormSchema = z.object({
name: z.string().nonempty("Name is required"), name: z.string().nonempty("Name is required")
siteIds: z.array(
z.object({
id: z.string(),
text: z.string()
})
)
}); });
type GeneralFormValues = z.infer<typeof GeneralFormSchema>; type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
@@ -54,15 +48,11 @@ export default function GeneralPage() {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const router = useRouter(); const router = useRouter();
const [sites, setSites] = useState<Tag[]>([]);
const [clientSites, setClientSites] = useState<Tag[]>([]);
const [activeSitesTagIndex, setActiveSitesTagIndex] = useState<number | null>(null);
const form = useForm({ const form = useForm({
resolver: zodResolver(GeneralFormSchema), resolver: zodResolver(GeneralFormSchema),
defaultValues: { defaultValues: {
name: client?.name, name: client?.name
siteIds: []
}, },
mode: "onChange" mode: "onChange"
}); });
@@ -75,23 +65,6 @@ export default function GeneralPage() {
const res = await api.get<AxiosResponse<ListSitesResponse>>( const res = await api.get<AxiosResponse<ListSitesResponse>>(
`/org/${client?.orgId}/sites/` `/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) { } catch (e) {
toast({ toast({
variant: "destructive", variant: "destructive",
@@ -114,8 +87,7 @@ export default function GeneralPage() {
try { try {
await api.post(`/client/${client?.clientId}`, { await api.post(`/client/${client?.clientId}`, {
name: data.name, name: data.name
siteIds: data.siteIds.map(site => parseInt(site.id))
}); });
updateClient({ name: data.name }); updateClient({ name: data.name });
@@ -130,10 +102,7 @@ export default function GeneralPage() {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t("clientUpdateFailed"), title: t("clientUpdateFailed"),
description: formatAxiosError( description: formatAxiosError(e, t("clientUpdateError"))
e,
t("clientUpdateError")
)
}); });
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -112,11 +112,11 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
siteName: siteResource.siteName, siteName: siteResource.siteName,
siteAddress: siteResource.siteAddress || null, siteAddress: siteResource.siteAddress || null,
mode: siteResource.mode || ("port" as any), mode: siteResource.mode || ("port" as any),
protocol: siteResource.protocol, // protocol: siteResource.protocol,
proxyPort: siteResource.proxyPort, // proxyPort: siteResource.proxyPort,
siteId: siteResource.siteId, siteId: siteResource.siteId,
destination: siteResource.destination, destination: siteResource.destination,
destinationPort: siteResource.destinationPort, // destinationPort: siteResource.destinationPort,
alias: siteResource.alias || null, alias: siteResource.alias || null,
siteNiceId: siteResource.siteNiceId siteNiceId: siteResource.siteNiceId
}; };

View File

@@ -90,7 +90,7 @@ export default function CreateInternalResourceDialog({
mode: z.enum(["host", "cidr"]), mode: z.enum(["host", "cidr"]),
destination: z.string().min(1), destination: z.string().min(1),
siteId: z.int().positive(t("createInternalResourceDialogPleaseSelectSite")), siteId: z.int().positive(t("createInternalResourceDialogPleaseSelectSite")),
protocol: z.enum(["tcp", "udp"]), // protocol: z.enum(["tcp", "udp"]),
// proxyPort: z.int() // proxyPort: z.int()
// .positive() // .positive()
// .min(1, t("createInternalResourceDialogProxyPortMin")) // .min(1, t("createInternalResourceDialogProxyPortMin"))
@@ -177,7 +177,7 @@ export default function CreateInternalResourceDialog({
name: "", name: "",
siteId: availableSites[0]?.siteId || 0, siteId: availableSites[0]?.siteId || 0,
mode: "host", mode: "host",
protocol: "tcp", // protocol: "tcp",
// proxyPort: undefined, // proxyPort: undefined,
destination: "", destination: "",
// destinationPort: undefined, // destinationPort: undefined,
@@ -196,7 +196,7 @@ export default function CreateInternalResourceDialog({
name: "", name: "",
siteId: availableSites[0].siteId, siteId: availableSites[0].siteId,
mode: "host", mode: "host",
protocol: "tcp", // protocol: "tcp",
// proxyPort: undefined, // proxyPort: undefined,
destination: "", destination: "",
// destinationPort: undefined, // destinationPort: undefined,
@@ -260,35 +260,38 @@ export default function CreateInternalResourceDialog({
{ {
name: data.name, name: data.name,
mode: data.mode, mode: data.mode,
protocol: data.mode === "port" ? data.protocol : undefined, // protocol: data.protocol,
// proxyPort: data.mode === "port" ? data.proxyPort : undefined, // proxyPort: data.mode === "port" ? data.proxyPort : undefined,
// destinationPort: data.mode === "port" ? data.destinationPort : undefined, // destinationPort: data.mode === "port" ? data.destinationPort : undefined,
destination: data.destination, destination: data.destination,
enabled: true, 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; const siteResourceId = response.data.data.siteResourceId;
// Set roles and users if provided // // Set roles and users if provided
if (data.roles && data.roles.length > 0) { // if (data.roles && data.roles.length > 0) {
await api.post(`/site-resource/${siteResourceId}/roles`, { // await api.post(`/site-resource/${siteResourceId}/roles`, {
roleIds: data.roles.map((r) => parseInt(r.id)) // roleIds: data.roles.map((r) => parseInt(r.id))
}); // });
} // }
if (data.users && data.users.length > 0) { // if (data.users && data.users.length > 0) {
await api.post(`/site-resource/${siteResourceId}/users`, { // await api.post(`/site-resource/${siteResourceId}/users`, {
userIds: data.users.map((u) => u.id) // userIds: data.users.map((u) => u.id)
}); // });
} // }
if (data.clients && data.clients.length > 0) { // if (data.clients && data.clients.length > 0) {
await api.post(`/site-resource/${siteResourceId}/clients`, { // await api.post(`/site-resource/${siteResourceId}/clients`, {
clientIds: data.clients.map((c) => parseInt(c.id)) // clientIds: data.clients.map((c) => parseInt(c.id))
}); // });
} // }
toast({ toast({
title: t("createInternalResourceDialogSuccess"), title: t("createInternalResourceDialogSuccess"),
@@ -444,7 +447,7 @@ export default function CreateInternalResourceDialog({
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem> {/* <SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem> */}
<SelectItem value="host">{t("createInternalResourceDialogModeHost")}</SelectItem> <SelectItem value="host">{t("createInternalResourceDialogModeHost")}</SelectItem>
<SelectItem value="cidr">{t("createInternalResourceDialogModeCidr")}</SelectItem> <SelectItem value="cidr">{t("createInternalResourceDialogModeCidr")}</SelectItem>
</SelectContent> </SelectContent>
@@ -535,7 +538,7 @@ export default function CreateInternalResourceDialog({
<FormDescription> <FormDescription>
{mode === "host" && t("createInternalResourceDialogDestinationHostDescription")} {mode === "host" && t("createInternalResourceDialogDestinationHostDescription")}
{mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")} {mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")}
{mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} {/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>

View File

@@ -36,7 +36,6 @@ import { toast } from "@app/hooks/useToast";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { Separator } from "@app/components/ui/separator";
import { ListRolesResponse } from "@server/routers/role"; import { ListRolesResponse } from "@server/routers/role";
import { ListUsersResponse } from "@server/routers/user"; import { ListUsersResponse } from "@server/routers/user";
import { ListSiteResourceRolesResponse } from "@server/routers/siteResource/listSiteResourceRoles"; import { ListSiteResourceRolesResponse } from "@server/routers/siteResource/listSiteResourceRoles";
@@ -52,12 +51,13 @@ type InternalResourceData = {
name: string; name: string;
orgId: string; orgId: string;
siteName: string; siteName: string;
mode: "host" | "cidr" | "port"; // mode: "host" | "cidr" | "port";
protocol: string | null; mode: "host" | "cidr";
proxyPort: number | null; // protocol: string | null;
// proxyPort: number | null;
siteId: number; siteId: number;
destination: string; destination: string;
destinationPort?: number | null; // destinationPort?: number | null;
alias?: string | null; alias?: string | null;
}; };
@@ -83,10 +83,10 @@ export default function EditInternalResourceDialog({
const formSchema = z.object({ const formSchema = z.object({
name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")),
mode: z.enum(["host", "cidr", "port"]), mode: z.enum(["host", "cidr", "port"]),
protocol: z.enum(["tcp", "udp"]).nullish(), // protocol: z.enum(["tcp", "udp"]).nullish(),
proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(), // proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
destination: z.string().min(1), 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(), alias: z.string().nullish(),
roles: z.array( roles: z.array(
z.object({ z.object({
@@ -107,42 +107,42 @@ export default function EditInternalResourceDialog({
}) })
).optional() ).optional()
}) })
.refine( // .refine(
(data) => { // (data) => {
if (data.mode === "port") { // if (data.mode === "port") {
return data.protocol !== undefined && data.protocol !== null; // return data.protocol !== undefined && data.protocol !== null;
} // }
return true; // return true;
}, // },
{ // {
message: t("editInternalResourceDialogProtocol") + " is required for port mode", // message: t("editInternalResourceDialogProtocol") + " is required for port mode",
path: ["protocol"] // path: ["protocol"]
} // }
) // )
.refine( // .refine(
(data) => { // (data) => {
if (data.mode === "port") { // if (data.mode === "port") {
return data.proxyPort !== undefined && data.proxyPort !== null; // return data.proxyPort !== undefined && data.proxyPort !== null;
} // }
return true; // return true;
}, // },
{ // {
message: t("editInternalResourceDialogSitePort") + " is required for port mode", // message: t("editInternalResourceDialogSitePort") + " is required for port mode",
path: ["proxyPort"] // path: ["proxyPort"]
} // }
) // )
.refine( // .refine(
(data) => { // (data) => {
if (data.mode === "port") { // if (data.mode === "port") {
return data.destinationPort !== undefined && data.destinationPort !== null; // return data.destinationPort !== undefined && data.destinationPort !== null;
} // }
return true; // return true;
}, // },
{ // {
message: t("targetPort") + " is required for port mode", // message: t("targetPort") + " is required for port mode",
path: ["destinationPort"] // path: ["destinationPort"]
} // }
); // );
type FormData = z.infer<typeof formSchema>; type FormData = z.infer<typeof formSchema>;
@@ -160,10 +160,10 @@ export default function EditInternalResourceDialog({
defaultValues: { defaultValues: {
name: resource.name, name: resource.name,
mode: resource.mode || "host", mode: resource.mode || "host",
protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, // protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
proxyPort: resource.proxyPort ?? undefined, // proxyPort: resource.proxyPort ?? undefined,
destination: resource.destination || "", destination: resource.destination || "",
destinationPort: resource.destinationPort ?? undefined, // destinationPort: resource.destinationPort ?? undefined,
alias: resource.alias ?? null, alias: resource.alias ?? null,
roles: [], roles: [],
users: [], users: [],
@@ -277,10 +277,10 @@ export default function EditInternalResourceDialog({
form.reset({ form.reset({
name: resource.name, name: resource.name,
mode: resource.mode || "host", mode: resource.mode || "host",
protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined, // protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
proxyPort: resource.proxyPort ?? undefined, // proxyPort: resource.proxyPort ?? undefined,
destination: resource.destination || "", destination: resource.destination || "",
destinationPort: resource.destinationPort ?? undefined, // destinationPort: resource.destinationPort ?? undefined,
alias: resource.alias ?? null, alias: resource.alias ?? null,
roles: [], roles: [],
users: [], users: [],
@@ -298,25 +298,28 @@ export default function EditInternalResourceDialog({
await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, {
name: data.name, name: data.name,
mode: data.mode, mode: data.mode,
protocol: data.mode === "port" ? data.protocol : null, // protocol: data.mode === "port" ? data.protocol : null,
proxyPort: data.mode === "port" ? data.proxyPort : null, // proxyPort: data.mode === "port" ? data.proxyPort : null,
destinationPort: data.mode === "port" ? data.destinationPort : null, // destinationPort: data.mode === "port" ? data.destinationPort : null,
destination: data.destination, 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 // Update roles, users, and clients
await Promise.all([ // await Promise.all([
api.post(`/site-resource/${resource.id}/roles`, { // api.post(`/site-resource/${resource.id}/roles`, {
roleIds: (data.roles || []).map((r) => parseInt(r.id)) // roleIds: (data.roles || []).map((r) => parseInt(r.id))
}), // }),
api.post(`/site-resource/${resource.id}/users`, { // api.post(`/site-resource/${resource.id}/users`, {
userIds: (data.users || []).map((u) => u.id) // userIds: (data.users || []).map((u) => u.id)
}), // }),
api.post(`/site-resource/${resource.id}/clients`, { // api.post(`/site-resource/${resource.id}/clients`, {
clientIds: (data.clients || []).map((c) => parseInt(c.id)) // clientIds: (data.clients || []).map((c) => parseInt(c.id))
}) // })
]); // ]);
toast({ toast({
title: t("editInternalResourceDialogSuccess"), title: t("editInternalResourceDialogSuccess"),
@@ -384,7 +387,7 @@ export default function EditInternalResourceDialog({
</SelectTrigger> </SelectTrigger>
</FormControl> </FormControl>
<SelectContent> <SelectContent>
<SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem> {/* <SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem> */}
<SelectItem value="host">{t("editInternalResourceDialogModeHost")}</SelectItem> <SelectItem value="host">{t("editInternalResourceDialogModeHost")}</SelectItem>
<SelectItem value="cidr">{t("editInternalResourceDialogModeCidr")}</SelectItem> <SelectItem value="cidr">{t("editInternalResourceDialogModeCidr")}</SelectItem>
</SelectContent> </SelectContent>
@@ -394,7 +397,7 @@ export default function EditInternalResourceDialog({
)} )}
/> />
{mode === "port" && ( {/* {mode === "port" && (
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<FormField <FormField
control={form.control} control={form.control}
@@ -439,7 +442,7 @@ export default function EditInternalResourceDialog({
)} )}
/> />
</div> </div>
)} )} */}
</div> </div>
</div> </div>
@@ -459,14 +462,14 @@ export default function EditInternalResourceDialog({
<FormDescription> <FormDescription>
{mode === "host" && t("editInternalResourceDialogDestinationHostDescription")} {mode === "host" && t("editInternalResourceDialogDestinationHostDescription")}
{mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")} {mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")}
{mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} {/* {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} */}
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
{mode === "port" && ( {/* {mode === "port" && (
<FormField <FormField
control={form.control} control={form.control}
name="destinationPort" name="destinationPort"
@@ -484,7 +487,7 @@ export default function EditInternalResourceDialog({
</FormItem> </FormItem>
)} )}
/> />
)} )} */}
</div> </div>
</div> </div>

View File

@@ -19,7 +19,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuCheckboxItem DropdownMenuCheckboxItem
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
@@ -164,13 +164,14 @@ export type InternalResourceRow = {
orgId: string; orgId: string;
siteName: string; siteName: string;
siteAddress: string | null; siteAddress: string | null;
mode: "host" | "cidr" | "port"; // mode: "host" | "cidr" | "port";
protocol: string | null; mode: "host" | "cidr";
proxyPort: number | null; // protocol: string | null;
// proxyPort: number | null;
siteId: number; siteId: number;
siteNiceId: string; siteNiceId: string;
destination: string; destination: string;
destinationPort: number | null; // destinationPort: number | null;
alias: string | null; alias: string | null;
}; };
@@ -515,10 +516,14 @@ export default function ResourcesTable({
> >
<StatusIcon status={overallStatus} /> <StatusIcon status={overallStatus} />
<span className="text-sm"> <span className="text-sm">
{overallStatus === "online" && t("resourcesTableHealthy")} {overallStatus === "online" &&
{overallStatus === "degraded" && t("resourcesTableDegraded")} t("resourcesTableHealthy")}
{overallStatus === "offline" && t("resourcesTableOffline")} {overallStatus === "degraded" &&
{overallStatus === "unknown" && t("resourcesTableUnknown")} t("resourcesTableDegraded")}
{overallStatus === "offline" &&
t("resourcesTableOffline")}
{overallStatus === "unknown" &&
t("resourcesTableUnknown")}
</span> </span>
<ChevronDown className="h-3 w-3" /> <ChevronDown className="h-3 w-3" />
</Button> </Button>
@@ -770,7 +775,11 @@ export default function ResourcesTable({
<div className="flex flex-col items-end gap-1 p-3"> <div className="flex flex-col items-end gap-1 p-3">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0"> <Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
>
<Columns className="h-4 w-4" /> <Columns className="h-4 w-4" />
<span className="sr-only"> <span className="sr-only">
{t("columns") || "Columns"} {t("columns") || "Columns"}
@@ -786,10 +795,14 @@ export default function ResourcesTable({
.getAllColumns() .getAllColumns()
.filter((column) => column.getCanHide()) .filter((column) => column.getCanHide())
.map((column) => { .map((column) => {
const columnDef = column.columnDef as any; const columnDef =
const friendlyName = columnDef.friendlyName; column.columnDef as any;
const displayName = friendlyName || const friendlyName =
(typeof columnDef.header === "string" columnDef.friendlyName;
const displayName =
friendlyName ||
(typeof columnDef.header ===
"string"
? columnDef.header ? columnDef.header
: column.id); : column.id);
return ( return (
@@ -798,9 +811,13 @@ export default function ResourcesTable({
className="capitalize" className="capitalize"
checked={column.getIsVisible()} checked={column.getIsVisible()}
onCheckedChange={(value) => onCheckedChange={(value) =>
column.toggleVisibility(!!value) column.toggleVisibility(
!!value
)
}
onSelect={(e) =>
e.preventDefault()
} }
onSelect={(e) => e.preventDefault()}
> >
{displayName} {displayName}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
@@ -925,23 +942,24 @@ export default function ResourcesTable({
let displayText: string; let displayText: string;
let copyText: string; let copyText: string;
if ( // if (
resourceRow.mode === "port" && // resourceRow.mode === "port" &&
resourceRow.protocol && // // resourceRow.protocol &&
resourceRow.proxyPort && // // resourceRow.proxyPort &&
resourceRow.destinationPort // // resourceRow.destinationPort
) { // ) {
const protocol = resourceRow.protocol.toUpperCase(); // // const protocol = resourceRow.protocol.toUpperCase();
// For port mode: site part uses alias or site address, destination part uses destination IP // // For port mode: site part uses alias or site address, destination part uses destination IP
// If site address has CIDR notation, extract just the IP address // // If site address has CIDR notation, extract just the IP address
let siteAddress = resourceRow.siteAddress; // let siteAddress = resourceRow.siteAddress;
if (siteAddress && siteAddress.includes("/")) { // if (siteAddress && siteAddress.includes("/")) {
siteAddress = siteAddress.split("/")[0]; // siteAddress = siteAddress.split("/")[0];
} // }
const siteDisplay = resourceRow.alias || siteAddress; // const siteDisplay = resourceRow.alias || siteAddress;
displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`; // // displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`;
copyText = `${siteDisplay}:${resourceRow.proxyPort}`; // // copyText = `${siteDisplay}:${resourceRow.proxyPort}`;
} else if (resourceRow.mode === "host") { // } else if (resourceRow.mode === "host") {
if (resourceRow.mode === "host") {
// For host mode: use alias if available, otherwise use destination // For host mode: use alias if available, otherwise use destination
const destinationDisplay = const destinationDisplay =
resourceRow.alias || resourceRow.destination; resourceRow.alias || resourceRow.destination;
@@ -981,7 +999,11 @@ export default function ResourcesTable({
<div className="flex flex-col items-end gap-1 p-3"> <div className="flex flex-col items-end gap-1 p-3">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-7 w-7 p-0"> <Button
variant="outline"
size="sm"
className="h-7 w-7 p-0"
>
<Columns className="h-4 w-4" /> <Columns className="h-4 w-4" />
<span className="sr-only"> <span className="sr-only">
{t("columns") || "Columns"} {t("columns") || "Columns"}
@@ -997,10 +1019,14 @@ export default function ResourcesTable({
.getAllColumns() .getAllColumns()
.filter((column) => column.getCanHide()) .filter((column) => column.getCanHide())
.map((column) => { .map((column) => {
const columnDef = column.columnDef as any; const columnDef =
const friendlyName = columnDef.friendlyName; column.columnDef as any;
const displayName = friendlyName || const friendlyName =
(typeof columnDef.header === "string" columnDef.friendlyName;
const displayName =
friendlyName ||
(typeof columnDef.header ===
"string"
? columnDef.header ? columnDef.header
: column.id); : column.id);
return ( return (
@@ -1009,9 +1035,13 @@ export default function ResourcesTable({
className="capitalize" className="capitalize"
checked={column.getIsVisible()} checked={column.getIsVisible()}
onCheckedChange={(value) => onCheckedChange={(value) =>
column.toggleVisibility(!!value) column.toggleVisibility(
!!value
)
}
onSelect={(e) =>
e.preventDefault()
} }
onSelect={(e) => e.preventDefault()}
> >
{displayName} {displayName}
</DropdownMenuCheckboxItem> </DropdownMenuCheckboxItem>
@@ -1230,99 +1260,111 @@ export default function ResourcesTable({
<TabsContent value="proxy"> <TabsContent value="proxy">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
{proxyTable {proxyTable
.getHeaderGroups() .getHeaderGroups()
.map((headerGroup) => ( .map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((header) =>
header.column.getIsVisible()
)
.map((header) => (
<TableHead
key={header.id}
className={`whitespace-nowrap ${
header.column.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: header.column.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{proxyTable.getRowModel().rows
?.length ? (
proxyTable
.getRowModel()
.rows.map((row) => (
<TableRow <TableRow
key={row.id} key={headerGroup.id}
data-state={
row.getIsSelected() &&
"selected"
}
> >
{row {headerGroup.headers
.getVisibleCells() .filter((header) =>
.map((cell) => ( header.column.getIsVisible()
<TableCell )
.map((header) => (
<TableHead
key={ key={
cell.id header.id
} }
className={`whitespace-nowrap ${ className={`whitespace-nowrap ${
cell.column.id === header
.column
.id ===
"actions" "actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card" ? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell.column.id === : header
"name" .column
? "md:sticky md:left-0 z-10 bg-card" .id ===
: "" "name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`} }`}
> >
{flexRender( {header.isPlaceholder
cell ? null
.column : flexRender(
.columnDef header
.cell, .column
cell.getContext() .columnDef
)} .header,
</TableCell> header.getContext()
)}
</TableHead>
))} ))}
</TableRow> </TableRow>
)) ))}
) : ( </TableHeader>
<TableRow> <TableBody>
<TableCell {proxyTable.getRowModel().rows
colSpan={ ?.length ? (
proxyColumns.length proxyTable
} .getRowModel()
className="h-24 text-center" .rows.map((row) => (
> <TableRow
{t( key={row.id}
"resourcesTableNoProxyResourcesFound" data-state={
)} row.getIsSelected() &&
</TableCell> "selected"
</TableRow> }
)} >
</TableBody> {row
</Table> .getVisibleCells()
.map((cell) => (
<TableCell
key={
cell.id
}
className={`whitespace-nowrap ${
cell
.column
.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell
.column
.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={
proxyColumns.length
}
className="h-24 text-center"
>
{t(
"resourcesTableNoProxyResourcesFound"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<DataTablePagination <DataTablePagination
@@ -1336,99 +1378,111 @@ export default function ResourcesTable({
<TabsContent value="internal"> <TabsContent value="internal">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
{internalTable {internalTable
.getHeaderGroups() .getHeaderGroups()
.map((headerGroup) => ( .map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((header) =>
header.column.getIsVisible()
)
.map((header) => (
<TableHead
key={header.id}
className={`whitespace-nowrap ${
header.column.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: header.column.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{internalTable.getRowModel().rows
?.length ? (
internalTable
.getRowModel()
.rows.map((row) => (
<TableRow <TableRow
key={row.id} key={headerGroup.id}
data-state={
row.getIsSelected() &&
"selected"
}
> >
{row {headerGroup.headers
.getVisibleCells() .filter((header) =>
.map((cell) => ( header.column.getIsVisible()
<TableCell )
.map((header) => (
<TableHead
key={ key={
cell.id header.id
} }
className={`whitespace-nowrap ${ className={`whitespace-nowrap ${
cell.column.id === header
.column
.id ===
"actions" "actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card" ? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell.column.id === : header
"name" .column
? "md:sticky md:left-0 z-10 bg-card" .id ===
: "" "name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`} }`}
> >
{flexRender( {header.isPlaceholder
cell ? null
.column : flexRender(
.columnDef header
.cell, .column
cell.getContext() .columnDef
)} .header,
</TableCell> header.getContext()
)}
</TableHead>
))} ))}
</TableRow> </TableRow>
)) ))}
) : ( </TableHeader>
<TableRow> <TableBody>
<TableCell {internalTable.getRowModel().rows
colSpan={ ?.length ? (
internalColumns.length internalTable
} .getRowModel()
className="h-24 text-center" .rows.map((row) => (
> <TableRow
{t( key={row.id}
"resourcesTableNoInternalResourcesFound" data-state={
)} row.getIsSelected() &&
</TableCell> "selected"
</TableRow> }
)} >
</TableBody> {row
</Table> .getVisibleCells()
.map((cell) => (
<TableCell
key={
cell.id
}
className={`whitespace-nowrap ${
cell
.column
.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell
.column
.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={
internalColumns.length
}
className="h-24 text-center"
>
{t(
"resourcesTableNoInternalResourcesFound"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<DataTablePagination <DataTablePagination