From 110e950476a51b730b81a13f918ef2565958c604 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 10:51:38 -0500 Subject: [PATCH 01/19] Send site name --- server/routers/olm/handleOlmRegisterMessage.ts | 1 + server/routers/olm/handleOlmServerPeerAddMessage.ts | 1 + server/routers/olm/peers.ts | 2 ++ 3 files changed, 4 insertions(+) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index cd7a308f..9cfbab0e 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -275,6 +275,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, + name: site.name, // relayEndpoint: relayEndpoint, // this can be undefined now if not relayed // lets not do this for now because it would conflict with the hole punch testing endpoint: site.endpoint, publicKey: site.publicKey, diff --git a/server/routers/olm/handleOlmServerPeerAddMessage.ts b/server/routers/olm/handleOlmServerPeerAddMessage.ts index 83f7ad8c..c0556b0e 100644 --- a/server/routers/olm/handleOlmServerPeerAddMessage.ts +++ b/server/routers/olm/handleOlmServerPeerAddMessage.ts @@ -169,6 +169,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async ( type: "olm/wg/peer/add", data: { siteId: site.siteId, + name: site.name, endpoint: site.endpoint, publicKey: site.publicKey, serverIP: site.address, diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 2708647b..4aa8edd7 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -8,6 +8,7 @@ export async function addPeer( clientId: number, peer: { siteId: number; + name: string; publicKey: string; endpoint: string; relayEndpoint: string; @@ -34,6 +35,7 @@ export async function addPeer( type: "olm/wg/peer/add", data: { siteId: peer.siteId, + name: peer.name, publicKey: peer.publicKey, endpoint: peer.endpoint, relayEndpoint: peer.relayEndpoint, From 5e9d660e267fc80ddf140fbc7a1707a886d4d5a4 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 11:07:08 -0500 Subject: [PATCH 02/19] We need to generate a niceId every time we make a client --- server/routers/client/createUserClient.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/routers/client/createUserClient.ts b/server/routers/client/createUserClient.ts index e5b5ea8f..5e9840f9 100644 --- a/server/routers/client/createUserClient.ts +++ b/server/routers/client/createUserClient.ts @@ -22,6 +22,7 @@ import { isIpInCidr } from "@server/lib/ip"; import { listExitNodes } from "#dynamic/lib/exitNodes"; import { OpenAPITags, registry } from "@server/openApi"; import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations"; +import { getUniqueClientName } from "@server/db/names"; const paramsSchema = z .object({ @@ -211,11 +212,14 @@ export async function createUserClient( ); } + const niceId = await getUniqueClientName(orgId); + [newClient] = await trx .insert(clients) .values({ exitNodeId: randomExitNode.exitNodeId, orgId, + niceId, name, subnet: updatedSubnet, type, From 38203a0e7c94f126dde10576e6a76922b364f0a2 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 11:10:34 -0500 Subject: [PATCH 03/19] adjustments to update notification --- messages/en-US.json | 2 +- src/components/ProductUpdates.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 9d668dfe..b8a0d1ea 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1320,7 +1320,7 @@ "productUpdateTitle": "Product Updates", "productUpdateEmpty": "No updates", "dismissAll": "Dismiss all", - "pangolinUpdateAvailable": "New version available", + "pangolinUpdateAvailable": "Update available", "pangolinUpdateAvailableInfo": "Version {version} is ready to install", "pangolinUpdateAvailableReleaseNotes": "View release notes", "newtUpdateAvailable": "Update Available", diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx index 73aaaf25..8645b5eb 100644 --- a/src/components/ProductUpdates.tsx +++ b/src/components/ProductUpdates.tsx @@ -356,7 +356,7 @@ function NewVersionAvailable({ -
+
{t("pangolinUpdateAvailableInfo", { version: version.pangolin.latestVersion @@ -365,7 +365,7 @@ function NewVersionAvailable({ {t("pangolinUpdateAvailableReleaseNotes")} From 311233b9f77f0d07d67a30b3ffaa100c87461b57 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 11:13:43 -0500 Subject: [PATCH 04/19] update remote node version col --- .../remote-exit-nodes/ExitNodesTable.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx index 62cada8d..a38f3b86 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx @@ -231,12 +231,22 @@ export default function ExitNodesTable({ }, cell: ({ row }) => { const originalRow = row.original; - return originalRow.version || "-"; + return ( +
+ {originalRow.version && originalRow.version ? ( + + {"v" + originalRow.version} + + ) : ( + "-" + )} +
+ ); } }, { id: "actions", - header: () => ({t("actions")}), + header: () => {t("actions")}, cell: ({ row }) => { const nodeRow = row.original; const remoteExitNodeId = nodeRow.id; @@ -295,9 +305,7 @@ export default function ExitNodesTable({ }} dialog={
-

- {t("remoteExitNodeQuestionRemove")} -

+

{t("remoteExitNodeQuestionRemove")}

{t("remoteExitNodeMessageRemove")}

From 9010803046c44f98b4fbaabc52addcab06f7dd48 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 11:40:06 -0500 Subject: [PATCH 05/19] fix verifySiteAccess middleware --- server/middlewares/verifySiteAccess.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/middlewares/verifySiteAccess.ts b/server/middlewares/verifySiteAccess.ts index 175dc565..05fc6d27 100644 --- a/server/middlewares/verifySiteAccess.ts +++ b/server/middlewares/verifySiteAccess.ts @@ -76,7 +76,7 @@ export async function verifySiteAccess( .select() .from(userOrgs) .where( - and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) + and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, site.orgId)) ) .limit(1); req.userOrg = userOrgRole[0]; From bc7a1f46731d7bc8d9bb06c35a5177a28c669dd2 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 11:45:58 -0500 Subject: [PATCH 06/19] change translation --- messages/en-US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index b8a0d1ea..3dd1c94e 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1320,9 +1320,9 @@ "productUpdateTitle": "Product Updates", "productUpdateEmpty": "No updates", "dismissAll": "Dismiss all", - "pangolinUpdateAvailable": "Update available", + "pangolinUpdateAvailable": "Update Available", "pangolinUpdateAvailableInfo": "Version {version} is ready to install", - "pangolinUpdateAvailableReleaseNotes": "View release notes", + "pangolinUpdateAvailableReleaseNotes": "View Release Notes", "newtUpdateAvailable": "Update Available", "newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.", "domainPickerEnterDomain": "Domain", From f66a9bdd33175f97ef04c10f4a6ffa48655130f0 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 11:47:19 -0500 Subject: [PATCH 07/19] only show updates number if more than one --- src/components/ProductUpdates.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx index 8645b5eb..f0857160 100644 --- a/src/components/ProductUpdates.tsx +++ b/src/components/ProductUpdates.tsx @@ -99,7 +99,7 @@ export default function ProductUpdates({ : "opacity-0" )} > - {filteredUpdates.length > 0 && ( + {filteredUpdates.length > 1 && ( <> From 2418813902a082f468a8f9ae3986a5e631a1cd4b Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 12:58:01 -0500 Subject: [PATCH 08/19] add sqlite migration --- server/middlewares/verifyOrgAccess.ts | 6 - server/setup/migrationsSqlite.ts | 4 +- server/setup/scriptsSqlite/1.13.0.ts | 360 ++++++++++++++++++++++++++ 3 files changed, 363 insertions(+), 7 deletions(-) create mode 100644 server/setup/scriptsSqlite/1.13.0.ts diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts index 74976553..729766ab 100644 --- a/server/middlewares/verifyOrgAccess.ts +++ b/server/middlewares/verifyOrgAccess.ts @@ -5,7 +5,6 @@ import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; -import logger from "@server/logger"; export async function verifyOrgAccess( req: Request, @@ -27,8 +26,6 @@ export async function verifyOrgAccess( ); } - logger.debug(`Verifying access for user ${userId} to organization ${orgId}`); - try { if (!req.userOrg) { const userOrgRes = await db @@ -71,9 +68,6 @@ export async function verifyOrgAccess( req.userOrgRoleId = req.userOrg.roleId; req.userOrgId = orgId; - logger.debug( - `User ${userId} has access to organization ${orgId} with role ${req.userOrg.roleId}` - ); return next(); } catch (e) { return next( diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index dd546db2..8ff66c49 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -32,6 +32,7 @@ import m27 from "./scriptsSqlite/1.10.2"; import m28 from "./scriptsSqlite/1.11.0"; import m29 from "./scriptsSqlite/1.11.1"; import m30 from "./scriptsSqlite/1.12.0"; +import m31 from "./scriptsSqlite/1.13.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -62,7 +63,8 @@ const migrations = [ { version: "1.10.2", run: m27 }, { version: "1.11.0", run: m28 }, { version: "1.11.1", run: m29 }, - { version: "1.12.0", run: m30 } + { version: "1.12.0", run: m30 }, + { version: "1.13.0", run: m31 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts new file mode 100644 index 00000000..76d0d07c --- /dev/null +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -0,0 +1,360 @@ +import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import { readFileSync } from "fs"; +import path, { join } from "path"; + +const version = "1.13.0"; + +const dev = process.env.ENVIRONMENT !== "prod"; +let file; +if (!dev) { + file = join(__DIRNAME, "names.json"); +} else { + file = join("server/db/names.json"); +} +export const names = JSON.parse(readFileSync(file, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // clean out any non-alphanumeric characters except for dashes + return name.replace(/[^a-z0-9-]/g, ""); +} + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + db.prepare( + `ALTER TABLE 'clientSites' RENAME TO 'clientSitesAssociationsCache';` + ).run(); + + db.prepare( + `ALTER TABLE 'clients' RENAME COLUMN 'id' TO 'clientId';` + ).run(); + + db.prepare( + ` + CREATE TABLE 'clientSiteResources' ( + 'clientId' integer NOT NULL, + 'siteResourceId' integer NOT NULL, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'clientSiteResourcesAssociationsCache' ( + 'clientId' integer NOT NULL, + 'siteResourceId' integer NOT NULL + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'deviceWebAuthCodes' ( + 'codeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'code' text NOT NULL, + 'ip' text, + 'city' text, + 'deviceName' text, + 'applicationName' text NOT NULL, + 'expiresAt' integer NOT NULL, + 'createdAt' integer NOT NULL, + 'verified' integer DEFAULT false NOT NULL, + 'userId' text, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `CREATE UNIQUE INDEX 'deviceWebAuthCodes_code_unique' ON 'deviceWebAuthCodes' ('code');` + ).run(); + + db.prepare( + ` + CREATE TABLE 'roleSiteResources' ( + 'roleId' integer NOT NULL, + 'siteResourceId' integer NOT NULL, + FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE 'userSiteResources' ( + 'userId' text NOT NULL, + 'siteResourceId' integer NOT NULL, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + ` + CREATE TABLE '__new_clientSitesAssociationsCache' ( + 'clientId' integer NOT NULL, + 'siteId' integer NOT NULL, + 'isRelayed' integer DEFAULT false NOT NULL, + 'endpoint' text, + 'publicKey' text + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_clientSitesAssociationsCache'("clientId", "siteId", "isRelayed", "endpoint", "publicKey") SELECT "clientId", "siteId", "isRelayed", "endpoint", NULL FROM 'clientSitesAssociationsCache';` + ).run(); + + db.prepare(`DROP TABLE 'clientSitesAssociationsCache';`).run(); + + db.prepare( + `ALTER TABLE '__new_clientSitesAssociationsCache' RENAME TO 'clientSitesAssociationsCache';` + ).run(); + + db.prepare( + `ALTER TABLE 'clients' ADD 'userId' text REFERENCES 'user'('id');` + ).run(); + + db.prepare( + `ALTER TABLE 'clients' ADD COLUMN 'niceId' TEXT NOT NULL DEFAULT 'PLACEHOLDER';` + ).run(); + + db.prepare(`ALTER TABLE 'clients' ADD 'olmId' text;`).run(); + + db.prepare( + ` + CREATE TABLE '__new_siteResources' ( + 'siteResourceId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'siteId' integer NOT NULL, + 'orgId' text NOT NULL, + 'niceId' text NOT NULL, + 'name' text NOT NULL, + 'mode' text NOT NULL, + 'protocol' text, + 'proxyPort' integer, + 'destinationPort' integer, + 'destination' text NOT NULL, + 'enabled' integer DEFAULT true NOT NULL, + 'alias' text, + 'aliasAddress' text, + FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_siteResources'("siteResourceId", "siteId", "orgId", "niceId", "name", "mode", "protocol", "proxyPort", "destinationPort", "destination", "enabled", "alias", "aliasAddress") SELECT "siteResourceId", "siteId", "orgId", "niceId", "name", 'host', "protocol", "proxyPort", "destinationPort", "destinationIp", "enabled", NULL, NULL FROM 'siteResources';` + ).run(); + + db.prepare(`DROP TABLE 'siteResources';`).run(); + + db.prepare( + `ALTER TABLE '__new_siteResources' RENAME TO 'siteResources';` + ).run(); + + db.prepare( + ` + CREATE TABLE '__new_olms' ( + 'id' text PRIMARY KEY NOT NULL, + 'secretHash' text NOT NULL, + 'dateCreated' text NOT NULL, + 'version' text, + 'agent' text, + 'name' text, + 'clientId' integer, + 'userId' text, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE set null, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_olms'("id", "secretHash", "dateCreated", "version", "agent", "name", "clientId", "userId") SELECT "id", "secretHash", "dateCreated", "version", NULL, NULL, "clientId", NULL FROM 'olms';` + ).run(); + + db.prepare(`DROP TABLE 'olms';`).run(); + + db.prepare(`ALTER TABLE '__new_olms' RENAME TO 'olms';`).run(); + + db.prepare( + ` + CREATE TABLE '__new_roleClients' ( + 'roleId' integer NOT NULL, + 'clientId' integer NOT NULL, + FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_roleClients'("roleId", "clientId") SELECT "roleId", "clientId" FROM 'roleClients';` + ).run(); + + db.prepare(`DROP TABLE 'roleClients';`).run(); + + db.prepare( + `ALTER TABLE '__new_roleClients' RENAME TO 'roleClients';` + ).run(); + + db.prepare( + ` + CREATE TABLE '__new_userClients' ( + 'userId' text NOT NULL, + 'clientId' integer NOT NULL, + FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade, + FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade + ); + ` + ).run(); + + db.prepare( + `INSERT INTO '__new_userClients'("userId", "clientId") SELECT "userId", "clientId" FROM 'userClients';` + ).run(); + + db.prepare(`DROP TABLE 'userClients';`).run(); + + db.prepare( + `ALTER TABLE '__new_userClients' RENAME TO 'userClients';` + ).run(); + + db.prepare(`ALTER TABLE 'orgs' ADD 'utilitySubnet' text;`).run(); + + db.prepare( + `ALTER TABLE 'session' ADD 'deviceAuthUsed' integer DEFAULT false NOT NULL;` + ).run(); + + db.prepare( + `ALTER TABLE 'targetHealthCheck' ADD 'hcTlsServerName' text;` + ).run(); + + db.prepare( + `ALTER TABLE 'sites' DROP COLUMN 'remoteSubnets';` + ).run(); + + // Associate all clients with each site resource + const clients = db + .prepare(`SELECT clientId FROM 'clients'`) + .all() as { + clientId: number; + }[]; + const siteResources = db + .prepare(`SELECT siteResourceId FROM 'siteResources'`) + .all() as { + siteResourceId: number; + }[]; + + const insertClientSiteResource = db.prepare( + `INSERT INTO 'clientSiteResources' ('clientId', 'siteResourceId') VALUES (?, ?)` + ); + + for (const client of clients) { + for (const siteResource of siteResources) { + insertClientSiteResource.run( + client.clientId, + siteResource.siteResourceId + ); + } + } + + // Associate existing site resources with their org's admin role + const siteResourcesWithOrg = db + .prepare( + `SELECT siteResourceId, orgId FROM 'siteResources'` + ) + .all() as { + siteResourceId: number; + orgId: string; + }[]; + + const getAdminRole = db.prepare( + `SELECT roleId FROM 'roles' WHERE orgId = ? AND isAdmin = 1 LIMIT 1` + ); + + const checkExistingAssociation = db.prepare( + `SELECT 1 FROM 'roleSiteResources' WHERE roleId = ? AND siteResourceId = ? LIMIT 1` + ); + + const insertRoleSiteResource = db.prepare( + `INSERT INTO 'roleSiteResources' ('roleId', 'siteResourceId') VALUES (?, ?)` + ); + + for (const siteResource of siteResourcesWithOrg) { + const adminRole = getAdminRole.get(siteResource.orgId) as + | { roleId: number } + | undefined; + + if (adminRole) { + const existing = checkExistingAssociation.get( + adminRole.roleId, + siteResource.siteResourceId + ); + + if (!existing) { + insertRoleSiteResource.run( + adminRole.roleId, + siteResource.siteResourceId + ); + } + } + } + + // Populate niceId for clients + const usedNiceIds: string[] = []; + + for (const clientId of clients) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + db.prepare( + `UPDATE clients SET niceId = ? WHERE clientId = ?` + ).run(niceId, clientId.clientId); + } + })(); + + db.pragma("foreign_keys = ON"); + + console.log(`Migrated database`); + } catch (e) { + console.log("Failed to migrate db:", e); + throw e; + } + + console.log(`${version} migration complete`); +} From 9221bcf8895070f0ae9e08a0d848492d4cf0844c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 13:50:05 -0500 Subject: [PATCH 09/19] add disconnect button to clients --- .../machine/[niceId]/credentials/page.tsx | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx index 9e0ad86b..881e6384 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx @@ -51,6 +51,7 @@ export default function CredentialsPage() { null ); const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -71,7 +72,8 @@ export default function CredentialsPage() { const rekeyRes = await api.post( `/re-key/${client?.clientId}/regenerate-client-secret`, { - secret: data.olmSecret + secret: data.olmSecret, + disconnect: shouldDisconnect } ); @@ -180,12 +182,27 @@ export default function CredentialsPage() { {build !== "oss" && ( - +
+ + +
)} @@ -204,11 +221,38 @@ export default function CredentialsPage() { }} dialog={
-

{t("regenerateCredentialsConfirmation")}

-

{t("regenerateCredentialsWarning")}

+ {shouldDisconnect ? ( + <> +

+ {t( + "clientRegenerateAndDisconnectConfirmation" + )} +

+

+ {t( + "clientRegenerateAndDisconnectWarning" + )} +

+ + ) : ( + <> +

+ {t( + "clientRegenerateCredentialsConfirmation" + )} +

+

+ {t("clientRegenerateCredentialsWarning")} +

+ + )}
} - buttonText={t("regenerateCredentialsButton")} + buttonText={ + shouldDisconnect + ? t("clientRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } onConfirm={handleConfirmRegenerate} string={getConfirmationString()} title={t("regenerateCredentials")} From 4d665e8596d387bba96c0ec648125647f973ca8b Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 14:30:06 -0500 Subject: [PATCH 10/19] Try to fix the expires at problem --- server/lib/traefik/TraefikConfigManager.ts | 36 +++++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/server/lib/traefik/TraefikConfigManager.ts b/server/lib/traefik/TraefikConfigManager.ts index 56648559..151e6517 100644 --- a/server/lib/traefik/TraefikConfigManager.ts +++ b/server/lib/traefik/TraefikConfigManager.ts @@ -142,8 +142,24 @@ export class TraefikConfigManager { const wildcardExists = await this.fileExists(wildcardPath); let lastModified: Date | null = null; - const expiresAt: Date | null = null; + let expiresAt: number | null = null; let wildcard = false; + const expiresAtPath = path.join(domainDir, ".expires_at"); + const expiresAtExists = await this.fileExists(expiresAtPath); + + if (expiresAtExists) { + try { + const expiresAtStr = fs + .readFileSync(expiresAtPath, "utf8") + .trim(); + expiresAt = parseInt(expiresAtStr, 10); + if (isNaN(expiresAt)) { + expiresAt = null; + } + } catch { + expiresAt = null; + } + } if (lastUpdateExists) { try { @@ -179,7 +195,7 @@ export class TraefikConfigManager { state.set(domain, { exists: certExists && keyExists, - lastModified, + lastModified: lastModified ? Math.floor(lastModified.getTime() / 1000) : null, expiresAt, wildcard }); @@ -259,9 +275,9 @@ export class TraefikConfigManager { // Check if certificate is expiring soon (within 30 days) if (localState.expiresAt) { - const daysUntilExpiry = - (localState.expiresAt - Math.floor(Date.now() / 1000)) / - (1000 * 60 * 60 * 24); + const nowInSeconds = Math.floor(Date.now() / 1000); + const secondsUntilExpiry = localState.expiresAt - nowInSeconds; + const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24); if (daysUntilExpiry < 30) { logger.info( `Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` @@ -770,6 +786,16 @@ export class TraefikConfigManager { "utf8" ); + // Store the certificate expiry time + if (cert.expiresAt) { + const expiresAtPath = path.join(domainDir, ".expires_at"); + fs.writeFileSync( + expiresAtPath, + cert.expiresAt.toString(), + "utf8" + ); + } + logger.info( `Certificate updated for domain: ${cert.domain}${cert.wildcard ? " (wildcard)" : ""}` ); From 5a60f66ae0865348c6f4217e9c549bf612c176d2 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 14:47:20 -0500 Subject: [PATCH 11/19] Update sqlite migration to update caches --- server/setup/scriptsSqlite/1.13.0.ts | 31 +++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts index 76d0d07c..86fa4244 100644 --- a/server/setup/scriptsSqlite/1.13.0.ts +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -271,6 +271,12 @@ export default async function migration() { const insertClientSiteResource = db.prepare( `INSERT INTO 'clientSiteResources' ('clientId', 'siteResourceId') VALUES (?, ?)` ); + const insertClientSiteResourceAssocCache = db.prepare( + `INSERT INTO 'clientSiteResourcesAssociationsCache' ('clientId', 'siteResourceId') VALUES (?, ?)` + ); + const insertClientSiteAssocCache = db.prepare( + `INSERT INTO 'clientSitesAssociationsCache' ('clientId', 'siteId', 'isRelayed', 'endpoint', 'publicKey') VALUES (?, ?, false, NULL, NULL)` + ); for (const client of clients) { for (const siteResource of siteResources) { @@ -278,14 +284,33 @@ export default async function migration() { client.clientId, siteResource.siteResourceId ); + insertClientSiteResourceAssocCache.run( + client.clientId, + siteResource.siteResourceId + ); + // check if clientSitesAssociationsCache already has an entry for this clientId and siteId + const siteIdRow = db + .prepare( + `SELECT siteId FROM 'siteResources' WHERE siteResourceId = ? LIMIT 1` + ) + .get(siteResource.siteResourceId) as { siteId: number }; + const existing = db + .prepare( + `SELECT 1 FROM 'clientSitesAssociationsCache' WHERE clientId = ? AND siteId = ? LIMIT 1` + ) + .get(client.clientId, siteIdRow.siteId); + if (!existing) { + insertClientSiteAssocCache.run( + client.clientId, + siteIdRow.siteId + ); + } } } // Associate existing site resources with their org's admin role const siteResourcesWithOrg = db - .prepare( - `SELECT siteResourceId, orgId FROM 'siteResources'` - ) + .prepare(`SELECT siteResourceId, orgId FROM 'siteResources'`) .all() as { siteResourceId: number; orgId: string; From 042c88ccb8649058f4c2d9b04f41d1c39a6b87f7 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 15:01:18 -0500 Subject: [PATCH 12/19] Calc session id correctly --- server/routers/olm/handleOlmPingMessage.ts | 8 +++- .../routers/olm/handleOlmRegisterMessage.ts | 39 +++++++++---------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 6859a00e..35d704c7 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -7,6 +7,8 @@ import logger from "@server/logger"; import { validateSessionToken } from "@server/auth/sessions/app"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { sendTerminateClient } from "../client/terminate"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; // Track if the offline checker interval is running let offlineCheckerInterval: NodeJS.Timeout | null = null; @@ -133,10 +135,14 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { return; } + const sessionId = encodeHexLowerCase( + sha256(new TextEncoder().encode(userToken)) + ); + const policyCheck = await checkOrgAccessPolicy({ orgId: client.orgId, userId: olm.userId, - sessionId: userToken // this is the user token passed in the message + sessionId // this is the user token passed in the message }); if (!policyCheck.allowed) { diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 9cfbab0e..0f71ee8b 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,17 +1,8 @@ import { - Client, clientSiteResourcesAssociationsCache, db, - ExitNode, - Org, orgs, - roleClients, - roles, - siteResources, - Transaction, - userClients, - userOrgs, - users + siteResources } from "@server/db"; import { MessageHandler } from "@server/routers/ws"; import { @@ -25,16 +16,13 @@ import { import { and, eq, inArray, isNull } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; -import { listExitNodes } from "#dynamic/lib/exitNodes"; -import { - generateAliasConfig, - getNextAvailableClientSubnet -} from "@server/lib/ip"; +import { generateAliasConfig } from "@server/lib/ip"; import { generateRemoteSubnets } from "@server/lib/ip"; -import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { validateSessionToken } from "@server/auth/sessions/app"; import config from "@server/lib/config"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); @@ -48,7 +36,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } = message.data; + const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } = + message.data; if (!olm.clientId) { logger.warn("Olm client ID not found"); @@ -94,10 +83,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } + const sessionId = encodeHexLowerCase( + sha256(new TextEncoder().encode(userToken)) + ); + const policyCheck = await checkOrgAccessPolicy({ orgId: orgId, userId: olm.userId, - sessionId: userToken // this is the user token passed in the message + sessionId // this is the user token passed in the message }); if (!policyCheck.allowed) { @@ -117,7 +110,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - if ((olmVersion && olm.version !== olmVersion) || (olmAgent && olm.agent !== olmAgent)) { + if ( + (olmVersion && olm.version !== olmVersion) || + (olmAgent && olm.agent !== olmAgent) + ) { await db .update(olms) .set({ @@ -175,7 +171,10 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } // Process each site - for (const { sites: site, clientSitesAssociationsCache: association } of sitesData) { + for (const { + sites: site, + clientSitesAssociationsCache: association + } of sitesData) { if (!site.exitNodeId) { logger.warn( `Site ${site.siteId} does not have exit node, skipping` From 40c38fa07081397d2fdc5ecfdccf7ef40aa8b561 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 15:19:40 -0500 Subject: [PATCH 13/19] Clear the associations first --- server/setup/scriptsSqlite/1.13.0.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts index 86fa4244..d477b4e0 100644 --- a/server/setup/scriptsSqlite/1.13.0.ts +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -271,6 +271,11 @@ export default async function migration() { const insertClientSiteResource = db.prepare( `INSERT INTO 'clientSiteResources' ('clientId', 'siteResourceId') VALUES (?, ?)` ); + + // clear the clientSiteResourcesAssociationsCache and clientSitesAssociationsCache tables to prepare for repopulation + db.prepare(`DELETE FROM 'clientSiteResourcesAssociationsCache';`).run(); + db.prepare(`DELETE FROM 'clientSitesAssociationsCache';`).run(); + const insertClientSiteResourceAssocCache = db.prepare( `INSERT INTO 'clientSiteResourcesAssociationsCache' ('clientId', 'siteResourceId') VALUES (?, ?)` ); From eecfcd640cd5cd258950d417bc54155fd329bff8 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sun, 7 Dec 2025 15:30:10 -0500 Subject: [PATCH 14/19] add pg and modify sqlite --- server/setup/scriptsPg/1.13.0.ts | 258 +++++++++++++++++++++++++++ server/setup/scriptsSqlite/1.13.0.ts | 69 +++---- 2 files changed, 285 insertions(+), 42 deletions(-) create mode 100644 server/setup/scriptsPg/1.13.0.ts diff --git a/server/setup/scriptsPg/1.13.0.ts b/server/setup/scriptsPg/1.13.0.ts new file mode 100644 index 00000000..31492765 --- /dev/null +++ b/server/setup/scriptsPg/1.13.0.ts @@ -0,0 +1,258 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; +import { __DIRNAME } from "@server/lib/consts"; +import { readFileSync } from "fs"; +import { join } from "path"; + +const version = "1.13.0"; + +const dev = process.env.ENVIRONMENT !== "prod"; +let file; +if (!dev) { + file = join(__DIRNAME, "names.json"); +} else { + file = join("server/db/names.json"); +} +export const names = JSON.parse(readFileSync(file, "utf-8")); + +export function generateName(): string { + const name = ( + names.descriptors[ + Math.floor(Math.random() * names.descriptors.length) + ] + + "-" + + names.animals[Math.floor(Math.random() * names.animals.length)] + ) + .toLowerCase() + .replace(/\s/g, "-"); + + // clean out any non-alphanumeric characters except for dashes + return name.replace(/[^a-z0-9-]/g, ""); +} + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql`BEGIN`); + + await db.execute(sql` + CREATE TABLE "clientSiteResources" ( + "clientId" integer NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "clientSiteResourcesAssociationsCache" ( + "clientId" integer NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "deviceWebAuthCodes" ( + "codeId" serial PRIMARY KEY NOT NULL, + "code" text NOT NULL, + "ip" text, + "city" text, + "deviceName" text, + "applicationName" text NOT NULL, + "expiresAt" bigint NOT NULL, + "createdAt" bigint NOT NULL, + "verified" boolean DEFAULT false NOT NULL, + "userId" varchar, + CONSTRAINT "deviceWebAuthCodes_code_unique" UNIQUE("code") + ); + `); + + await db.execute(sql` + CREATE TABLE "roleSiteResources" ( + "roleId" integer NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql` + CREATE TABLE "userSiteResources" ( + "userId" varchar NOT NULL, + "siteResourceId" integer NOT NULL + ); + `); + + await db.execute(sql`ALTER TABLE "clientSites" RENAME TO "clientSitesAssociationsCache";`); + + await db.execute(sql`ALTER TABLE "clients" RENAME COLUMN "id" TO "clientId";`); + + await db.execute(sql`ALTER TABLE "siteResources" RENAME COLUMN "destinationIp" TO "destination";`); + + await db.execute(sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_clientId_clients_id_fk";`); + + await db.execute(sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_siteId_sites_siteId_fk";`); + + await db.execute(sql`ALTER TABLE "olms" DROP CONSTRAINT "olms_clientId_clients_id_fk";`); + + await db.execute(sql`ALTER TABLE "roleClients" DROP CONSTRAINT "roleClients_clientId_clients_id_fk";`); + + await db.execute(sql`ALTER TABLE "userClients" DROP CONSTRAINT "userClients_clientId_clients_id_fk";`); + + await db.execute(sql`ALTER TABLE "siteResources" ALTER COLUMN "protocol" DROP NOT NULL;`); + + await db.execute(sql`ALTER TABLE "siteResources" ALTER COLUMN "proxyPort" DROP NOT NULL;`); + + await db.execute(sql`ALTER TABLE "siteResources" ALTER COLUMN "destinationPort" DROP NOT NULL;`); + + await db.execute(sql`ALTER TABLE "clientSitesAssociationsCache" ADD COLUMN "publicKey" varchar;`); + + await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "userId" text;`); + + await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "niceId" varchar NOT NULL DEFAULT 'PLACEHOLDER';`); + + await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "olmId" text;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "agent" text;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "name" varchar;`); + + await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "userId" text;`); + + await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "utilitySubnet" varchar;`); + + await db.execute(sql`ALTER TABLE "session" ADD COLUMN "deviceAuthUsed" boolean DEFAULT false NOT NULL;`); + + await db.execute(sql`ALTER TABLE "siteResources" ADD COLUMN "mode" varchar NOT NULL DEFAULT 'host';`); + + await db.execute(sql`ALTER TABLE "siteResources" ADD COLUMN "alias" varchar;`); + + await db.execute(sql`ALTER TABLE "siteResources" ADD COLUMN "aliasAddress" varchar;`); + + await db.execute(sql`ALTER TABLE "targetHealthCheck" ADD COLUMN "hcTlsServerName" text;`); + + await db.execute(sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "deviceWebAuthCodes" ADD CONSTRAINT "deviceWebAuthCodes_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "clients" ADD CONSTRAINT "clients_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE set null ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "roleClients" ADD CONSTRAINT "roleClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "userClients" ADD CONSTRAINT "userClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + + await db.execute(sql`ALTER TABLE "sites" DROP COLUMN "remoteSubnets";`); + + // Associate clients with site resources based on their previous site access + // Get all client-site associations from the renamed clientSitesAssociationsCache table + const clientSiteAssociationsQuery = await db.execute(sql` + SELECT "clientId", "siteId" FROM "clientSitesAssociationsCache" + `); + const clientSiteAssociations = clientSiteAssociationsQuery.rows as { + clientId: number; + siteId: number; + }[]; + + // For each client-site association, find all site resources for that site + for (const association of clientSiteAssociations) { + const siteResourcesQuery = await db.execute(sql` + SELECT "siteResourceId" FROM "siteResources" + WHERE "siteId" = ${association.siteId} + `); + const siteResources = siteResourcesQuery.rows as { + siteResourceId: number; + }[]; + + // Associate the client with all site resources from this site + for (const siteResource of siteResources) { + await db.execute(sql` + INSERT INTO "clientSiteResources" ("clientId", "siteResourceId") + VALUES (${association.clientId}, ${siteResource.siteResourceId}) + `); + } + } + + // Associate existing site resources with their org's admin role + const siteResourcesWithOrgQuery = await db.execute(sql` + SELECT "siteResourceId", "orgId" FROM "siteResources" + `); + const siteResourcesWithOrg = siteResourcesWithOrgQuery.rows as { + siteResourceId: number; + orgId: string; + }[]; + + for (const siteResource of siteResourcesWithOrg) { + const adminRoleQuery = await db.execute(sql` + SELECT "roleId" FROM "roles" WHERE "orgId" = ${siteResource.orgId} AND "isAdmin" = true LIMIT 1 + `); + const adminRole = adminRoleQuery.rows[0] as + | { roleId: number } + | undefined; + + if (adminRole) { + const existingQuery = await db.execute(sql` + SELECT 1 FROM "roleSiteResources" + WHERE "roleId" = ${adminRole.roleId} AND "siteResourceId" = ${siteResource.siteResourceId} + LIMIT 1 + `); + + if (existingQuery.rows.length === 0) { + await db.execute(sql` + INSERT INTO "roleSiteResources" ("roleId", "siteResourceId") + VALUES (${adminRole.roleId}, ${siteResource.siteResourceId}) + `); + } + } + } + + // Populate niceId for clients + const clientsQuery = await db.execute(sql`SELECT "clientId" FROM "clients"`); + const clients = clientsQuery.rows as { + clientId: number; + }[]; + + const usedNiceIds: string[] = []; + + for (const client of clients) { + // Generate a unique name and ensure it's unique + let niceId = ""; + let loops = 0; + while (true) { + if (loops > 100) { + throw new Error("Could not generate a unique name"); + } + + niceId = generateName(); + if (!usedNiceIds.includes(niceId)) { + usedNiceIds.push(niceId); + break; + } + loops++; + } + await db.execute(sql` + UPDATE "clients" SET "niceId" = ${niceId} WHERE "clientId" = ${client.clientId} + `); + } + + await db.execute(sql`COMMIT`); + console.log("Migrated database"); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to migrate database"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts index d477b4e0..aa599f0f 100644 --- a/server/setup/scriptsSqlite/1.13.0.ts +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -256,66 +256,45 @@ export default async function migration() { `ALTER TABLE 'sites' DROP COLUMN 'remoteSubnets';` ).run(); - // Associate all clients with each site resource - const clients = db - .prepare(`SELECT clientId FROM 'clients'`) + // Associate clients with site resources based on their previous site access + // Get all client-site associations from the renamed clientSitesAssociationsCache table + const clientSiteAssociations = db + .prepare(`SELECT clientId, siteId FROM 'clientSitesAssociationsCache'`) .all() as { clientId: number; + siteId: number; }[]; - const siteResources = db - .prepare(`SELECT siteResourceId FROM 'siteResources'`) - .all() as { - siteResourceId: number; - }[]; + + const getSiteResources = db.prepare( + `SELECT siteResourceId FROM 'siteResources' WHERE siteId = ?` + ); const insertClientSiteResource = db.prepare( `INSERT INTO 'clientSiteResources' ('clientId', 'siteResourceId') VALUES (?, ?)` ); - // clear the clientSiteResourcesAssociationsCache and clientSitesAssociationsCache tables to prepare for repopulation - db.prepare(`DELETE FROM 'clientSiteResourcesAssociationsCache';`).run(); - db.prepare(`DELETE FROM 'clientSitesAssociationsCache';`).run(); + // For each client-site association, find all site resources for that site + for (const association of clientSiteAssociations) { + const siteResources = getSiteResources.all( + association.siteId + ) as { + siteResourceId: number; + }[]; - const insertClientSiteResourceAssocCache = db.prepare( - `INSERT INTO 'clientSiteResourcesAssociationsCache' ('clientId', 'siteResourceId') VALUES (?, ?)` - ); - const insertClientSiteAssocCache = db.prepare( - `INSERT INTO 'clientSitesAssociationsCache' ('clientId', 'siteId', 'isRelayed', 'endpoint', 'publicKey') VALUES (?, ?, false, NULL, NULL)` - ); - - for (const client of clients) { + // Associate the client with all site resources from this site for (const siteResource of siteResources) { insertClientSiteResource.run( - client.clientId, + association.clientId, siteResource.siteResourceId ); - insertClientSiteResourceAssocCache.run( - client.clientId, - siteResource.siteResourceId - ); - // check if clientSitesAssociationsCache already has an entry for this clientId and siteId - const siteIdRow = db - .prepare( - `SELECT siteId FROM 'siteResources' WHERE siteResourceId = ? LIMIT 1` - ) - .get(siteResource.siteResourceId) as { siteId: number }; - const existing = db - .prepare( - `SELECT 1 FROM 'clientSitesAssociationsCache' WHERE clientId = ? AND siteId = ? LIMIT 1` - ) - .get(client.clientId, siteIdRow.siteId); - if (!existing) { - insertClientSiteAssocCache.run( - client.clientId, - siteIdRow.siteId - ); - } } } // Associate existing site resources with their org's admin role const siteResourcesWithOrg = db - .prepare(`SELECT siteResourceId, orgId FROM 'siteResources'`) + .prepare( + `SELECT siteResourceId, orgId FROM 'siteResources'` + ) .all() as { siteResourceId: number; orgId: string; @@ -354,6 +333,12 @@ export default async function migration() { } // Populate niceId for clients + const clients = db + .prepare(`SELECT clientId FROM 'clients'`) + .all() as { + clientId: number; + }[]; + const usedNiceIds: string[] = []; for (const clientId of clients) { From a3ba4fff54b3fd8d4a6fd578429d90b5991ad518 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 17:57:22 -0500 Subject: [PATCH 15/19] Bump version to 1.13.0-rc.0 --- server/lib/consts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/consts.ts b/server/lib/consts.ts index d93cf224..b380023e 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.13.0"; +export const APP_VERSION = "1.13.0-rc.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); From e8f10b049ebd63262e827f94a61556a393255fd2 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 20:04:30 -0500 Subject: [PATCH 16/19] Generate resources for remote subnets --- server/setup/migrationsPg.ts | 4 +- server/setup/scriptsPg/1.13.0.ts | 41 +++++++++++++++++++ server/setup/scriptsSqlite/1.13.0.ts | 60 ++++++++++++++++++++++++++-- 3 files changed, 100 insertions(+), 5 deletions(-) diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index f3b07bec..c778cca3 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -14,6 +14,7 @@ import m6 from "./scriptsPg/1.10.2"; import m7 from "./scriptsPg/1.11.0"; import m8 from "./scriptsPg/1.11.1"; import m9 from "./scriptsPg/1.12.0"; +import m10 from "./scriptsPg/1.13.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -28,7 +29,8 @@ const migrations = [ { version: "1.10.2", run: m6 }, { version: "1.11.0", run: m7 }, { version: "1.11.1", run: m8 }, - { version: "1.12.0", run: m9 } + { version: "1.12.0", run: m9 }, + { version: "1.13.0", run: m10 }, // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/scriptsPg/1.13.0.ts b/server/setup/scriptsPg/1.13.0.ts index 31492765..777e3718 100644 --- a/server/setup/scriptsPg/1.13.0.ts +++ b/server/setup/scriptsPg/1.13.0.ts @@ -152,8 +152,49 @@ export default async function migration() { await db.execute(sql`ALTER TABLE "userClients" ADD CONSTRAINT "userClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + // set 100.96.128.0/24 as the utility subnet on all of the orgs + await db.execute(sql`UPDATE "orgs" SET "utilitySubnet" = '100.96.128.0/24'`); + + // Query all of the sites to get their remoteSubnets + + const sitesRemoteSubnetsData = await db.execute(sql`SELECT "siteId", "remoteSubnets" FROM "sites" WHERE "remoteSubnets" IS NOT NULL + `); + const sitesRemoteSubnets = sitesRemoteSubnetsData.rows as { + siteId: number; + remoteSubnets: string | null; + }[]; + await db.execute(sql`ALTER TABLE "sites" DROP COLUMN "remoteSubnets";`); + + // get all of the siteResources and set the the aliasAddress to 100.96.128.x starting at .8 + const siteResourcesData = await db.execute(sql`SELECT "siteResourceId" FROM "siteResources" ORDER BY "siteResourceId" ASC`); + const siteResources = siteResourcesData.rows as { + siteResourceId: number; + }[]; + + let aliasIpOctet = 8; + for (const siteResource of siteResources) { + const aliasAddress = `100.96.128.${aliasIpOctet}`; + await db.execute(sql` + UPDATE "siteResources" SET "aliasAddress" = ${aliasAddress} WHERE "siteResourceId" = ${siteResource.siteResourceId} + `); + aliasIpOctet++; + } + + // For each site with remote subnets we need to create a site resource of type cidr for each remote subnet + for (const site of sitesRemoteSubnets) { + if (site.remoteSubnets) { + const subnets = site.remoteSubnets.split(","); + for (const subnet of subnets) { + await db.execute(sql` + INSERT INTO "siteResources" ("siteId", "destination", "mode", "name") + VALUES (${site.siteId}, ${subnet.trim()}, 'cidr', 'Remote Subnet'); + `); + } + } + } + // Associate clients with site resources based on their previous site access // Get all client-site associations from the renamed clientSitesAssociationsCache table const clientSiteAssociationsQuery = await db.execute(sql` diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts index aa599f0f..d74e3ea4 100644 --- a/server/setup/scriptsSqlite/1.13.0.ts +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -252,14 +252,68 @@ export default async function migration() { `ALTER TABLE 'targetHealthCheck' ADD 'hcTlsServerName' text;` ).run(); + // set 100.96.128.0/24 as the utility subnet on all of the orgs + db.prepare( + `UPDATE 'orgs' SET 'utilitySubnet' = '100.96.128.0/24'` + ).run(); + + // Query all of the sites to get their remoteSubnets before dropping the column + const sitesRemoteSubnets = db + .prepare( + `SELECT siteId, remoteSubnets FROM 'sites' WHERE remoteSubnets IS NOT NULL` + ) + .all() as { + siteId: number; + remoteSubnets: string | null; + }[]; + db.prepare( `ALTER TABLE 'sites' DROP COLUMN 'remoteSubnets';` ).run(); + // get all of the siteResources and set the aliasAddress to 100.96.128.x starting at .8 + const siteResourcesForAlias = db + .prepare( + `SELECT siteResourceId FROM 'siteResources' ORDER BY siteResourceId ASC` + ) + .all() as { + siteResourceId: number; + }[]; + + const updateAliasAddress = db.prepare( + `UPDATE 'siteResources' SET aliasAddress = ? WHERE siteResourceId = ?` + ); + + let aliasIpOctet = 8; + for (const siteResource of siteResourcesForAlias) { + const aliasAddress = `100.96.128.${aliasIpOctet}`; + updateAliasAddress.run(aliasAddress, siteResource.siteResourceId); + aliasIpOctet++; + } + + // For each site with remote subnets we need to create a site resource of type cidr for each remote subnet + const insertCidrResource = db.prepare( + `INSERT INTO 'siteResources' ('siteId', 'destination', 'mode', 'name', 'orgId', 'niceId') + SELECT ?, ?, 'cidr', 'Remote Subnet', orgId, ? FROM 'sites' WHERE siteId = ?` + ); + + for (const site of sitesRemoteSubnets) { + if (site.remoteSubnets) { + const subnets = site.remoteSubnets.split(","); + for (const subnet of subnets) { + // Generate a unique niceId for each new site resource + let niceId = generateName(); + insertCidrResource.run(site.siteId, subnet.trim(), niceId, site.siteId); + } + } + } + // Associate clients with site resources based on their previous site access // Get all client-site associations from the renamed clientSitesAssociationsCache table const clientSiteAssociations = db - .prepare(`SELECT clientId, siteId FROM 'clientSitesAssociationsCache'`) + .prepare( + `SELECT clientId, siteId FROM 'clientSitesAssociationsCache'` + ) .all() as { clientId: number; siteId: number; @@ -292,9 +346,7 @@ export default async function migration() { // Associate existing site resources with their org's admin role const siteResourcesWithOrg = db - .prepare( - `SELECT siteResourceId, orgId FROM 'siteResources'` - ) + .prepare(`SELECT siteResourceId, orgId FROM 'siteResources'`) .all() as { siteResourceId: number; orgId: string; From 1d7f4322e35a2c2f3aa55f1ffd67cc590f8ef45c Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 21:14:36 -0500 Subject: [PATCH 17/19] Migrations working --- server/setup/scriptsPg/1.13.0.ts | 161 ++++++++++++++++++++------- server/setup/scriptsSqlite/1.13.0.ts | 9 ++ 2 files changed, 130 insertions(+), 40 deletions(-) diff --git a/server/setup/scriptsPg/1.13.0.ts b/server/setup/scriptsPg/1.13.0.ts index 777e3718..e13276df 100644 --- a/server/setup/scriptsPg/1.13.0.ts +++ b/server/setup/scriptsPg/1.13.0.ts @@ -80,33 +80,59 @@ export default async function migration() { ); `); - await db.execute(sql`ALTER TABLE "clientSites" RENAME TO "clientSitesAssociationsCache";`); + await db.execute( + sql`ALTER TABLE "clientSites" RENAME TO "clientSitesAssociationsCache";` + ); - await db.execute(sql`ALTER TABLE "clients" RENAME COLUMN "id" TO "clientId";`); + await db.execute( + sql`ALTER TABLE "clients" RENAME COLUMN "id" TO "clientId";` + ); - await db.execute(sql`ALTER TABLE "siteResources" RENAME COLUMN "destinationIp" TO "destination";`); + await db.execute( + sql`ALTER TABLE "siteResources" RENAME COLUMN "destinationIp" TO "destination";` + ); - await db.execute(sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_clientId_clients_id_fk";`); + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_clientId_clients_id_fk";` + ); - await db.execute(sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_siteId_sites_siteId_fk";`); + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" DROP CONSTRAINT "clientSites_siteId_sites_siteId_fk";` + ); - await db.execute(sql`ALTER TABLE "olms" DROP CONSTRAINT "olms_clientId_clients_id_fk";`); + await db.execute( + sql`ALTER TABLE "olms" DROP CONSTRAINT "olms_clientId_clients_id_fk";` + ); - await db.execute(sql`ALTER TABLE "roleClients" DROP CONSTRAINT "roleClients_clientId_clients_id_fk";`); + await db.execute( + sql`ALTER TABLE "roleClients" DROP CONSTRAINT "roleClients_clientId_clients_id_fk";` + ); - await db.execute(sql`ALTER TABLE "userClients" DROP CONSTRAINT "userClients_clientId_clients_id_fk";`); + await db.execute( + sql`ALTER TABLE "userClients" DROP CONSTRAINT "userClients_clientId_clients_id_fk";` + ); - await db.execute(sql`ALTER TABLE "siteResources" ALTER COLUMN "protocol" DROP NOT NULL;`); + await db.execute( + sql`ALTER TABLE "siteResources" ALTER COLUMN "protocol" DROP NOT NULL;` + ); - await db.execute(sql`ALTER TABLE "siteResources" ALTER COLUMN "proxyPort" DROP NOT NULL;`); + await db.execute( + sql`ALTER TABLE "siteResources" ALTER COLUMN "proxyPort" DROP NOT NULL;` + ); - await db.execute(sql`ALTER TABLE "siteResources" ALTER COLUMN "destinationPort" DROP NOT NULL;`); + await db.execute( + sql`ALTER TABLE "siteResources" ALTER COLUMN "destinationPort" DROP NOT NULL;` + ); - await db.execute(sql`ALTER TABLE "clientSitesAssociationsCache" ADD COLUMN "publicKey" varchar;`); + await db.execute( + sql`ALTER TABLE "clientSitesAssociationsCache" ADD COLUMN "publicKey" varchar;` + ); await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "userId" text;`); - await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "niceId" varchar NOT NULL DEFAULT 'PLACEHOLDER';`); + await db.execute( + sql`ALTER TABLE "clients" ADD COLUMN "niceId" varchar NOT NULL DEFAULT 'PLACEHOLDER';` + ); await db.execute(sql`ALTER TABLE "clients" ADD COLUMN "olmId" text;`); @@ -116,59 +142,99 @@ export default async function migration() { await db.execute(sql`ALTER TABLE "olms" ADD COLUMN "userId" text;`); - await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "utilitySubnet" varchar;`); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "utilitySubnet" varchar;` + ); - await db.execute(sql`ALTER TABLE "session" ADD COLUMN "deviceAuthUsed" boolean DEFAULT false NOT NULL;`); + await db.execute( + sql`ALTER TABLE "session" ADD COLUMN "deviceAuthUsed" boolean DEFAULT false NOT NULL;` + ); - await db.execute(sql`ALTER TABLE "siteResources" ADD COLUMN "mode" varchar NOT NULL DEFAULT 'host';`); + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "mode" varchar NOT NULL DEFAULT 'host';` + ); - await db.execute(sql`ALTER TABLE "siteResources" ADD COLUMN "alias" varchar;`); + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "alias" varchar;` + ); - await db.execute(sql`ALTER TABLE "siteResources" ADD COLUMN "aliasAddress" varchar;`); + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "aliasAddress" varchar;` + ); - await db.execute(sql`ALTER TABLE "targetHealthCheck" ADD COLUMN "hcTlsServerName" text;`); + await db.execute( + sql`ALTER TABLE "targetHealthCheck" ADD COLUMN "hcTlsServerName" text;` + ); - await db.execute(sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "clientSiteResources" ADD CONSTRAINT "clientSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "deviceWebAuthCodes" ADD CONSTRAINT "deviceWebAuthCodes_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "deviceWebAuthCodes" ADD CONSTRAINT "deviceWebAuthCodes_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "roleSiteResources" ADD CONSTRAINT "roleSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "userSiteResources" ADD CONSTRAINT "userSiteResources_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "clients" ADD CONSTRAINT "clients_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "clients" ADD CONSTRAINT "clients_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE set null ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE set null ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "olms" ADD CONSTRAINT "olms_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "roleClients" ADD CONSTRAINT "roleClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "roleClients" ADD CONSTRAINT "roleClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); - await db.execute(sql`ALTER TABLE "userClients" ADD CONSTRAINT "userClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`); + await db.execute( + sql`ALTER TABLE "userClients" ADD CONSTRAINT "userClients_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;` + ); // set 100.96.128.0/24 as the utility subnet on all of the orgs - await db.execute(sql`UPDATE "orgs" SET "utilitySubnet" = '100.96.128.0/24'`); + await db.execute( + sql`UPDATE "orgs" SET "utilitySubnet" = '100.96.128.0/24'` + ); // Query all of the sites to get their remoteSubnets - - const sitesRemoteSubnetsData = await db.execute(sql`SELECT "siteId", "remoteSubnets" FROM "sites" WHERE "remoteSubnets" IS NOT NULL + + const sitesRemoteSubnetsData = + await db.execute(sql`SELECT "siteId", "remoteSubnets" FROM "sites" WHERE "remoteSubnets" IS NOT NULL `); const sitesRemoteSubnets = sitesRemoteSubnetsData.rows as { siteId: number; remoteSubnets: string | null; }[]; - + await db.execute(sql`ALTER TABLE "sites" DROP COLUMN "remoteSubnets";`); - // get all of the siteResources and set the the aliasAddress to 100.96.128.x starting at .8 - const siteResourcesData = await db.execute(sql`SELECT "siteResourceId" FROM "siteResources" ORDER BY "siteResourceId" ASC`); + const siteResourcesData = await db.execute( + sql`SELECT "siteResourceId" FROM "siteResources" ORDER BY "siteResourceId" ASC` + ); const siteResources = siteResourcesData.rows as { siteResourceId: number; }[]; @@ -185,11 +251,19 @@ export default async function migration() { // For each site with remote subnets we need to create a site resource of type cidr for each remote subnet for (const site of sitesRemoteSubnets) { if (site.remoteSubnets) { + // Get the orgId for this site + const siteDataQuery = await db.execute(sql` + SELECT "orgId" FROM "sites" WHERE "siteId" = ${site.siteId} + `); + const siteData = siteDataQuery.rows[0] as { orgId: string } | undefined; + if (!siteData) continue; + const subnets = site.remoteSubnets.split(","); for (const subnet of subnets) { + const niceId = generateName(); await db.execute(sql` - INSERT INTO "siteResources" ("siteId", "destination", "mode", "name") - VALUES (${site.siteId}, ${subnet.trim()}, 'cidr', 'Remote Subnet'); + INSERT INTO "siteResources" ("siteId", "orgId", "niceId", "destination", "mode", "name") + VALUES (${site.siteId}, ${siteData.orgId}, ${niceId}, ${subnet.trim()}, 'cidr', 'Remote Subnet'); `); } } @@ -221,6 +295,11 @@ export default async function migration() { INSERT INTO "clientSiteResources" ("clientId", "siteResourceId") VALUES (${association.clientId}, ${siteResource.siteResourceId}) `); + // also associate in the clientSiteResourcesAssociationsCache table + await db.execute(sql` + INSERT INTO "clientSiteResourcesAssociationsCache" ("clientId", "siteResourceId") + VALUES (${association.clientId}, ${siteResource.siteResourceId}) + `); } } @@ -258,7 +337,9 @@ export default async function migration() { } // Populate niceId for clients - const clientsQuery = await db.execute(sql`SELECT "clientId" FROM "clients"`); + const clientsQuery = await db.execute( + sql`SELECT "clientId" FROM "clients"` + ); const clients = clientsQuery.rows as { clientId: number; }[]; diff --git a/server/setup/scriptsSqlite/1.13.0.ts b/server/setup/scriptsSqlite/1.13.0.ts index d74e3ea4..5b2bcf01 100644 --- a/server/setup/scriptsSqlite/1.13.0.ts +++ b/server/setup/scriptsSqlite/1.13.0.ts @@ -327,6 +327,11 @@ export default async function migration() { `INSERT INTO 'clientSiteResources' ('clientId', 'siteResourceId') VALUES (?, ?)` ); + // create a clientSiteResourcesAssociationsCache entry for each existing association as well + const insertClientSiteResourceCache = db.prepare( + `INSERT INTO 'clientSiteResourcesAssociationsCache' ('clientId', 'siteResourceId') VALUES (?, ?)` + ); + // For each client-site association, find all site resources for that site for (const association of clientSiteAssociations) { const siteResources = getSiteResources.all( @@ -341,6 +346,10 @@ export default async function migration() { association.clientId, siteResource.siteResourceId ); + insertClientSiteResourceCache.run( + association.clientId, + siteResource.siteResourceId + ); } } From e10f7efcbe747bb9466d60f072df742e9ee4b848 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 22:00:55 -0500 Subject: [PATCH 18/19] Fix blueprints zod update --- server/lib/blueprints/types.ts | 166 ++++++++++++++++----------------- 1 file changed, 79 insertions(+), 87 deletions(-) diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index bf513461..9a184a1f 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -328,111 +328,103 @@ export const ConfigSchema = z sites: Record>; }; }) - .refine( + .superRefine((config, ctx) => { // Enforce the full-domain uniqueness across resources in the same stack - (config) => { - // Extract duplicates for error message - const fullDomainMap = new Map(); + const fullDomainMap = new Map(); - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const fullDomain = resource["full-domain"]; - if (fullDomain) { - // Only process if full-domain is defined - if (!fullDomainMap.has(fullDomain)) { - fullDomainMap.set(fullDomain, []); - } - fullDomainMap.get(fullDomain)!.push(resourceKey); + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const fullDomain = resource["full-domain"]; + if (fullDomain) { + // Only process if full-domain is defined + if (!fullDomainMap.has(fullDomain)) { + fullDomainMap.set(fullDomain, []); } + fullDomainMap.get(fullDomain)!.push(resourceKey); } - ); - - const duplicates = Array.from(fullDomainMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([fullDomain, resourceKeys]) => - `'${fullDomain}' used by resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - if (duplicates.length !== 0) { - return { - path: ["resources"], - error: `Duplicate 'full-domain' values found: ${duplicates}` - }; } + ); + + const fullDomainDuplicates = Array.from(fullDomainMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([fullDomain, resourceKeys]) => + `'${fullDomain}' used by resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + if (fullDomainDuplicates.length !== 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["proxy-resources"], + message: `Duplicate 'full-domain' values found: ${fullDomainDuplicates}` + }); } - ) - .refine( + // Enforce proxy-port uniqueness within proxy-resources per protocol - (config) => { - // Extract duplicates for error message - const protocolPortMap = new Map(); + const protocolPortMap = new Map(); - Object.entries(config["proxy-resources"]).forEach( - ([resourceKey, resource]) => { - const proxyPort = resource["proxy-port"]; - const protocol = resource.protocol; - if (proxyPort !== undefined && protocol !== undefined) { - const key = `${protocol}:${proxyPort}`; - if (!protocolPortMap.has(key)) { - protocolPortMap.set(key, []); - } - protocolPortMap.get(key)!.push(resourceKey); + Object.entries(config["proxy-resources"]).forEach( + ([resourceKey, resource]) => { + const proxyPort = resource["proxy-port"]; + const protocol = resource.protocol; + if (proxyPort !== undefined && protocol !== undefined) { + const key = `${protocol}:${proxyPort}`; + if (!protocolPortMap.has(key)) { + protocolPortMap.set(key, []); } + protocolPortMap.get(key)!.push(resourceKey); } - ); - - const duplicates = Array.from(protocolPortMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map(([protocolPort, resourceKeys]) => { - const [protocol, port] = protocolPort.split(":"); - return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`; - }) - .join("; "); - - if (duplicates.length !== 0) { - return { - path: ["proxy-resources"], - error: `Duplicate 'proxy-port' values found in proxy-resources: ${duplicates}` - }; } + ); + + const portDuplicates = Array.from(protocolPortMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map(([protocolPort, resourceKeys]) => { + const [protocol, port] = protocolPort.split(":"); + return `${protocol.toUpperCase()} port ${port} used by proxy-resources: ${resourceKeys.join(", ")}`; + }) + .join("; "); + + if (portDuplicates.length !== 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["proxy-resources"], + message: `Duplicate 'proxy-port' values found in proxy-resources: ${portDuplicates}` + }); } - ) - .refine( + // Enforce alias uniqueness within client-resources - (config) => { - // Extract duplicates for error message - const aliasMap = new Map(); + const aliasMap = new Map(); - Object.entries(config["client-resources"]).forEach( - ([resourceKey, resource]) => { - const alias = resource.alias; - if (alias !== undefined) { - if (!aliasMap.has(alias)) { - aliasMap.set(alias, []); - } - aliasMap.get(alias)!.push(resourceKey); + Object.entries(config["client-resources"]).forEach( + ([resourceKey, resource]) => { + const alias = resource.alias; + if (alias !== undefined) { + if (!aliasMap.has(alias)) { + aliasMap.set(alias, []); } + aliasMap.get(alias)!.push(resourceKey); } - ); - - const duplicates = Array.from(aliasMap.entries()) - .filter(([_, resourceKeys]) => resourceKeys.length > 1) - .map( - ([alias, resourceKeys]) => - `alias '${alias}' used by client-resources: ${resourceKeys.join(", ")}` - ) - .join("; "); - - if (duplicates.length !== 0) { - return { - path: ["client-resources"], - error: `Duplicate 'alias' values found in client-resources: ${duplicates}` - }; } + ); + + const aliasDuplicates = Array.from(aliasMap.entries()) + .filter(([_, resourceKeys]) => resourceKeys.length > 1) + .map( + ([alias, resourceKeys]) => + `alias '${alias}' used by client-resources: ${resourceKeys.join(", ")}` + ) + .join("; "); + + if (aliasDuplicates.length !== 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["client-resources"], + message: `Duplicate 'alias' values found in client-resources: ${aliasDuplicates}` + }); } - ); + }); // Type inference from the schema export type Site = z.infer; From 24cdac95cde6a7681489c0a510ec4f528c65fe29 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 7 Dec 2025 22:13:26 -0500 Subject: [PATCH 19/19] Fix not rebuilding site resources from blueprint --- server/lib/blueprints/applyBlueprint.ts | 17 +++++++---------- .../routers/siteResource/updateSiteResource.ts | 8 ++++++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index 86a18c8c..6168f85d 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -133,15 +133,12 @@ export async function applyBlueprint({ `Updating client resource ${result.newSiteResource.siteResourceId} on site ${site.sites.siteId}` ); - if (result.oldSiteResource) { - // this was an update - await handleMessagingForUpdatedSiteResource( - result.oldSiteResource, - result.newSiteResource, - { siteId: site.sites.siteId, orgId: site.sites.orgId }, - trx - ); - } + await handleMessagingForUpdatedSiteResource( + result.oldSiteResource, + result.newSiteResource, + { siteId: site.sites.siteId, orgId: site.sites.orgId }, + trx + ); // await addClientTargets( // site.newt.newtId, @@ -188,4 +185,4 @@ export async function applyBlueprint({ } return blueprint; -} \ No newline at end of file +} diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 9161c509..efc4939b 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -328,23 +328,27 @@ export async function updateSiteResource( } export async function handleMessagingForUpdatedSiteResource( - existingSiteResource: SiteResource, + existingSiteResource: SiteResource | undefined, updatedSiteResource: SiteResource, site: { siteId: number; orgId: string }, trx: Transaction ) { const { mergedAllClients } = await rebuildClientAssociationsFromSiteResource( - existingSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below + existingSiteResource || updatedSiteResource, // we want to rebuild based on the existing resource then we will apply the change to the destination below trx ); // after everything is rebuilt above we still need to update the targets and remote subnets if the destination changed const destinationChanged = + existingSiteResource && existingSiteResource.destination !== updatedSiteResource.destination; const aliasChanged = + existingSiteResource && existingSiteResource.alias !== updatedSiteResource.alias; + // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all + if (destinationChanged || aliasChanged) { const [newt] = await trx .select()