From 71da22328c8539ec28f66a97446eb4b96e1e6e4b Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 6 Jul 2026 11:13:11 -0400 Subject: [PATCH 1/7] Quiet logs --- server/lib/blueprints/applyNewtDockerBlueprint.ts | 2 +- server/routers/blueprints/applyJSONBlueprint.ts | 2 +- server/routers/newt/handleApplyBlueprintMessage.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts index 6b91b3462..59e7103d8 100644 --- a/server/lib/blueprints/applyNewtDockerBlueprint.ts +++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts @@ -116,7 +116,7 @@ export async function applyNewtDockerBlueprint( source: "NEWT" }); } catch (error) { - logger.error(`Failed to update database from config: ${error}`); + logger.debug(`Failed to update database from config: ${error}`); await sendToClient(newtId, { type: "newt/blueprint/results", data: { diff --git a/server/routers/blueprints/applyJSONBlueprint.ts b/server/routers/blueprints/applyJSONBlueprint.ts index 8ad41e9e4..ae231f8c8 100644 --- a/server/routers/blueprints/applyJSONBlueprint.ts +++ b/server/routers/blueprints/applyJSONBlueprint.ts @@ -99,7 +99,7 @@ export async function applyJSONBlueprint( source: "API" }); } catch (error) { - logger.error(`Failed to update database from config: ${error}`); + logger.debug(`Failed to update database from config: ${error}`); return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/routers/newt/handleApplyBlueprintMessage.ts b/server/routers/newt/handleApplyBlueprintMessage.ts index 908ff048a..ccc1a6a82 100644 --- a/server/routers/newt/handleApplyBlueprintMessage.ts +++ b/server/routers/newt/handleApplyBlueprintMessage.ts @@ -50,7 +50,7 @@ export const handleApplyBlueprintMessage: MessageHandler = async (context) => { source: "NEWT" }); } catch (error) { - logger.error(`Failed to update database from config: ${error}`); + logger.debug(`Failed to update database from config: ${error}`); return { message: { type: "newt/blueprint/results", From d4549f1c336f84fcad24105c89f6ab7b9deaf0fc Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 6 Jul 2026 14:44:16 -0400 Subject: [PATCH 2/7] log error --- server/private/middlewares/verifySubscription.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/private/middlewares/verifySubscription.ts b/server/private/middlewares/verifySubscription.ts index 92d5d9cfe..a2dce7818 100644 --- a/server/private/middlewares/verifySubscription.ts +++ b/server/private/middlewares/verifySubscription.ts @@ -65,6 +65,7 @@ export function verifyValidSubscription(tiers: Tier[]) { return next(); } catch (e) { + console.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, From 633159fb77955432d8f218ea4143666dfd0c6f99 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 7 Jul 2026 08:37:20 -0400 Subject: [PATCH 3/7] Email whitelist should respect policies --- .../resource/addEmailToResourceWhitelist.ts | 130 ++++++++++++---- .../routers/resource/getResourceWhitelist.ts | 16 +- .../removeEmailFromResourceWhitelist.ts | 145 +++++++++++++----- .../routers/resource/setResourceWhitelist.ts | 13 +- 4 files changed, 222 insertions(+), 82 deletions(-) diff --git a/server/routers/resource/addEmailToResourceWhitelist.ts b/server/routers/resource/addEmailToResourceWhitelist.ts index 506c2caa9..6d5ef22cd 100644 --- a/server/routers/resource/addEmailToResourceWhitelist.ts +++ b/server/routers/resource/addEmailToResourceWhitelist.ts @@ -1,7 +1,12 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, resourceWhitelist } from "@server/db"; +import { + resources, + resourceWhitelist, + resourcePolicies, + resourcePolicyWhiteList +} from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -103,40 +108,99 @@ export async function addEmailToResourceWhitelist( ); } - if (!resource.emailWhitelistEnabled) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Email whitelist is not enabled for this resource" - ) - ); + // A shared policy takes precedence over the resource's inline + // (default) policy, which takes precedence over the resource's own + // direct whitelist fields. This mirrors the precedence used at + // request time in authWithWhitelist.ts / getResourceAuthInfo.ts. + const policyId = + resource.resourcePolicyId ?? resource.defaultResourcePolicyId; + + if (policyId !== null) { + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, policyId)); + + if (!policy) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Resource policy not found" + ) + ); + } + + if (!policy.emailWhitelistEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Email whitelist is not enabled for this resource" + ) + ); + } + + const existingEntry = await db + .select() + .from(resourcePolicyWhiteList) + .where( + and( + eq( + resourcePolicyWhiteList.resourcePolicyId, + policyId + ), + eq(resourcePolicyWhiteList.email, email) + ) + ); + + if (existingEntry.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Email already exists in whitelist" + ) + ); + } + + await db.insert(resourcePolicyWhiteList).values({ + email, + resourcePolicyId: policyId + }); + } else { + if (!resource.emailWhitelistEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Email whitelist is not enabled for this resource" + ) + ); + } + + // Check if email already exists in whitelist + const existingEntry = await db + .select() + .from(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq(resourceWhitelist.email, email) + ) + ); + + if (existingEntry.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Email already exists in whitelist" + ) + ); + } + + await db.insert(resourceWhitelist).values({ + email, + resourceId + }); } - // Check if email already exists in whitelist - const existingEntry = await db - .select() - .from(resourceWhitelist) - .where( - and( - eq(resourceWhitelist.resourceId, resourceId), - eq(resourceWhitelist.email, email) - ) - ); - - if (existingEntry.length > 0) { - return next( - createHttpError( - HttpCode.CONFLICT, - "Email already exists in whitelist" - ) - ); - } - - await db.insert(resourceWhitelist).values({ - email, - resourceId - }); - return response(res, { data: {}, success: true, diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index 2d882d49e..0bebe6a71 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -96,13 +96,17 @@ export async function getResourceWhitelist( ); } - const isInlinePolicy = - resource.resourcePolicyId === null && - resource.defaultResourcePolicyId !== null; + // A shared policy takes precedence over the resource's inline + // (default) policy, which takes precedence over the resource's own + // direct whitelist fields. This mirrors the precedence used at + // request time in authWithWhitelist.ts / getResourceAuthInfo.ts. + const policyId = + resource.resourcePolicyId ?? resource.defaultResourcePolicyId; - const whitelist = isInlinePolicy - ? await queryPolicyWhitelist(resource.defaultResourcePolicyId!) - : await queryWhitelist(resourceId); + const whitelist = + policyId !== null + ? await queryPolicyWhitelist(policyId) + : await queryWhitelist(resourceId); return response(res, { data: { diff --git a/server/routers/resource/removeEmailFromResourceWhitelist.ts b/server/routers/resource/removeEmailFromResourceWhitelist.ts index 615e62438..03d961335 100644 --- a/server/routers/resource/removeEmailFromResourceWhitelist.ts +++ b/server/routers/resource/removeEmailFromResourceWhitelist.ts @@ -1,7 +1,12 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; -import { resources, resourceWhitelist } from "@server/db"; +import { + resources, + resourceWhitelist, + resourcePolicies, + resourcePolicyWhiteList +} from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -102,44 +107,110 @@ export async function removeEmailFromResourceWhitelist( ); } - if (!resource.emailWhitelistEnabled) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "Email whitelist is not enabled for this resource" - ) - ); + // A shared policy takes precedence over the resource's inline + // (default) policy, which takes precedence over the resource's own + // direct whitelist fields. This mirrors the precedence used at + // request time in authWithWhitelist.ts / getResourceAuthInfo.ts. + const policyId = + resource.resourcePolicyId ?? resource.defaultResourcePolicyId; + + if (policyId !== null) { + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, policyId)); + + if (!policy) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Resource policy not found" + ) + ); + } + + if (!policy.emailWhitelistEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Email whitelist is not enabled for this resource" + ) + ); + } + + const existingEntry = await db + .select() + .from(resourcePolicyWhiteList) + .where( + and( + eq( + resourcePolicyWhiteList.resourcePolicyId, + policyId + ), + eq(resourcePolicyWhiteList.email, email) + ) + ); + + if (existingEntry.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Email not found in whitelist" + ) + ); + } + + await db + .delete(resourcePolicyWhiteList) + .where( + and( + eq( + resourcePolicyWhiteList.resourcePolicyId, + policyId + ), + eq(resourcePolicyWhiteList.email, email) + ) + ); + } else { + if (!resource.emailWhitelistEnabled) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Email whitelist is not enabled for this resource" + ) + ); + } + + // Check if email exists in whitelist + const existingEntry = await db + .select() + .from(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq(resourceWhitelist.email, email) + ) + ); + + if (existingEntry.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Email not found in whitelist" + ) + ); + } + + await db + .delete(resourceWhitelist) + .where( + and( + eq(resourceWhitelist.resourceId, resourceId), + eq(resourceWhitelist.email, email) + ) + ); } - // Check if email exists in whitelist - const existingEntry = await db - .select() - .from(resourceWhitelist) - .where( - and( - eq(resourceWhitelist.resourceId, resourceId), - eq(resourceWhitelist.email, email) - ) - ); - - if (existingEntry.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - "Email not found in whitelist" - ) - ); - } - - await db - .delete(resourceWhitelist) - .where( - and( - eq(resourceWhitelist.resourceId, resourceId), - eq(resourceWhitelist.email, email) - ) - ); - return response(res, { data: {}, success: true, diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index 697ae4541..10bf5b480 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -109,13 +109,14 @@ export async function setResourceWhitelist( ); } - const isInlinePolicy = - resource.resourcePolicyId === null && - resource.defaultResourcePolicyId !== null; - - if (isInlinePolicy) { - const policyId = resource.defaultResourcePolicyId!; + // A shared policy takes precedence over the resource's inline + // (default) policy, which takes precedence over the resource's own + // direct whitelist fields. This mirrors the precedence used at + // request time in authWithWhitelist.ts / getResourceAuthInfo.ts. + const policyId = + resource.resourcePolicyId ?? resource.defaultResourcePolicyId; + if (policyId !== null) { const [policy] = await db .select() .from(resourcePolicies) From 8cceed91cf9bb01ee0b8e56a3c9e0d0348068c8f Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 7 Jul 2026 09:02:25 -0400 Subject: [PATCH 4/7] Make feilds optional --- .../siteResource/updateSiteResource.ts | 216 ++++++++++-------- 1 file changed, 125 insertions(+), 91 deletions(-) diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 22159f1c6..fbba73d4b 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -68,9 +68,9 @@ const updateSiteResourceSchema = z "Fully qualified domain name with optional wildcards, e.g., example.internal, *.example.internal, or host-0?.example.internal", example: "service.example.internal" }), - userIds: z.array(z.string()), - roleIds: z.array(z.int()), - clientIds: z.array(z.int()), + userIds: z.array(z.string()).optional(), + roleIds: z.array(z.int()).optional(), + clientIds: z.array(z.int()).optional(), tcpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema, disableIcmp: z.boolean().optional(), @@ -167,6 +167,10 @@ const updateSiteResourceSchema = z ) .refine( (data) => { + // if neither is provided, the existing site associations are left unchanged + if (data.siteIds === undefined && data.siteId === undefined) { + return true; + } return ( (data.siteIds !== undefined && data.siteIds.length > 0) || data.siteId !== undefined @@ -263,7 +267,7 @@ export async function updateSiteResource( const { siteResourceId } = parsedParams.data; const { name, - siteIds: siteIdsInput = [], // because it can change + siteIds: siteIdsInput, siteId, niceId, mode, @@ -287,9 +291,14 @@ export async function updateSiteResource( } = parsedBody.data; // Backward compatibility: merge deprecated siteId into siteIds array - const siteIds = [...siteIdsInput]; - if (siteId !== undefined && !siteIds.includes(siteId)) { - siteIds.push(siteId); + const siteIdsProvided = + siteIdsInput !== undefined || siteId !== undefined; + let siteIds: number[] | undefined; + if (siteIdsProvided) { + siteIds = [...(siteIdsInput ?? [])]; + if (siteId !== undefined && !siteIds.includes(siteId)) { + siteIds.push(siteId); + } } // Check if site resource exists @@ -356,20 +365,22 @@ export async function updateSiteResource( } // Verify the site exists and belongs to the org - const sitesToAssign = await db - .select() - .from(sites) - .where( - and( - inArray(sites.siteId, siteIds), - eq(sites.orgId, existingSiteResource.orgId) - ) - ); + if (siteIds !== undefined) { + const sitesToAssign = await db + .select() + .from(sites) + .where( + and( + inArray(sites.siteId, siteIds), + eq(sites.orgId, existingSiteResource.orgId) + ) + ); - if (sitesToAssign.length !== siteIds.length) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Some site not found") - ); + if (sitesToAssign.length !== siteIds.length) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Some site not found") + ); + } } // Only check if destination is an IP address @@ -463,7 +474,8 @@ export async function updateSiteResource( } let updatedSiteResource: SiteResource | undefined; - let updatedSiteIds: number[] = []; + // defaults to the existing sites; only overwritten below if siteIds/siteId was provided + let updatedSiteIds: number[] = [...existingSiteIds]; await db.transaction(async (trx) => { // Update the site resource const sshPamSet = @@ -522,81 +534,103 @@ export async function updateSiteResource( //////////////////// update the associations //////////////////// - // delete the site - site resources associations - await trx - .delete(siteNetworks) - .where( - eq(siteNetworks.networkId, updatedSiteResource.networkId!) - ); - - for (const siteId of siteIds) { - await trx.insert(siteNetworks).values({ - siteId: siteId, - networkId: updatedSiteResource.networkId! - }); - updatedSiteIds.push(siteId); - } - - await trx - .delete(clientSiteResources) - .where(eq(clientSiteResources.siteResourceId, siteResourceId)); - - if (clientIds.length > 0) { - await trx.insert(clientSiteResources).values( - clientIds.map((clientId) => ({ - clientId, - siteResourceId - })) - ); - } - - await trx - .delete(userSiteResources) - .where(eq(userSiteResources.siteResourceId, siteResourceId)); - - if (userIds.length > 0) { - 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 { + if (siteIds !== undefined) { + // delete the site - site resources associations await trx - .delete(roleSiteResources) + .delete(siteNetworks) .where( - eq(roleSiteResources.siteResourceId, siteResourceId) + eq( + siteNetworks.networkId, + updatedSiteResource.networkId! + ) ); + + updatedSiteIds = []; + for (const siteId of siteIds) { + await trx.insert(siteNetworks).values({ + siteId: siteId, + networkId: updatedSiteResource.networkId! + }); + updatedSiteIds.push(siteId); + } } - if (roleIds.length > 0) { - await trx.insert(roleSiteResources).values( - roleIds.map((roleId) => ({ - roleId, - siteResourceId - })) - ); + if (clientIds !== undefined) { + await trx + .delete(clientSiteResources) + .where( + eq(clientSiteResources.siteResourceId, siteResourceId) + ); + + if (clientIds.length > 0) { + await trx.insert(clientSiteResources).values( + clientIds.map((clientId) => ({ + clientId, + siteResourceId + })) + ); + } + } + + if (userIds !== undefined) { + await trx + .delete(userSiteResources) + .where( + eq(userSiteResources.siteResourceId, siteResourceId) + ); + + if (userIds.length > 0) { + await trx.insert(userSiteResources).values( + userIds.map((userId) => ({ + userId, + siteResourceId + })) + ); + } + } + + if (roleIds !== undefined) { + // Get all admin role IDs for this org to exclude from deletion + const adminRoles = await trx + .select() + .from(roles) + .where( + and( + eq(roles.isAdmin, true), + eq(roles.orgId, updatedSiteResource.orgId) + ) + ); + const adminRoleIds = adminRoles.map((role) => role.roleId); + + if (adminRoleIds.length > 0) { + await trx.delete(roleSiteResources).where( + and( + eq( + roleSiteResources.siteResourceId, + siteResourceId + ), + ne(roleSiteResources.roleId, adminRoleIds[0]) // delete all but the admin role + ) + ); + } else { + await trx + .delete(roleSiteResources) + .where( + eq( + roleSiteResources.siteResourceId, + siteResourceId + ) + ); + } + + if (roleIds.length > 0) { + await trx.insert(roleSiteResources).values( + roleIds.map((roleId) => ({ + roleId, + siteResourceId + })) + ); + } } logger.info(`Updated site resource ${siteResourceId}`); From e9d424eb806d43dccc37a45a4d75e09bb901c2f6 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 7 Jul 2026 10:41:12 -0400 Subject: [PATCH 5/7] Move jit mode to a config param Ref #3262 --- server/db/pg/poolConfig.ts | 35 ++++++++++++++++++++++++----------- server/lib/readConfigFile.ts | 3 ++- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/server/db/pg/poolConfig.ts b/server/db/pg/poolConfig.ts index af6c10520..2f3f93a62 100644 --- a/server/db/pg/poolConfig.ts +++ b/server/db/pg/poolConfig.ts @@ -1,3 +1,4 @@ +import config from "@server/lib/config"; import { Pool, PoolConfig } from "pg"; export function createPoolConfig( @@ -18,17 +19,7 @@ export function createPoolConfig( keepAliveInitialDelayMillis: 10000, // send first keepalive after 10s of idle // Allow connections to be released and recreated more aggressively // to avoid stale connections building up - allowExitOnIdle: false, - // Disable JIT compilation for this connection. Our hot-path queries - // (e.g. resource-by-domain lookups) join many tables but only ever - // return a handful of rows. When planner row estimates drift (e.g. - // due to autovacuum lag under write-heavy load), Postgres decides - // these plans are expensive enough to JIT-compile, which can add - // multiple seconds of pure compilation overhead per query and - // saturate the connection pool. JIT never pays off for these - // short-lived OLTP queries, so it's disabled outright rather than - // relying on statistics staying fresh. - options: "-c jit=off" + allowExitOnIdle: false }; } @@ -49,6 +40,28 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void { `Failed to set statement_timeout on ${label} client: ${err.message}` ); }); + + // Disable JIT compilation for this connection. Our hot-path queries + // (e.g. resource-by-domain lookups) join many tables but only ever + // return a handful of rows. When planner row estimates drift (e.g. + // due to autovacuum lag under write-heavy load), Postgres decides + // these plans are expensive enough to JIT-compile, which can add + // multiple seconds of pure compilation overhead per query and + // saturate the connection pool. JIT never pays off for these + // short-lived OLTP queries, so it's disabled outright rather than + // relying on statistics staying fresh. + // + // Set via a runtime SET command rather than the `options: "-c + // jit=off"` startup parameter: connections in SaaS mode go through + // a pooler (e.g. PgBouncer) that rejects arbitrary startup packet + // options with a protocol_violation (08P01) error. + if (config.getRawConfig().postgres?.pool.jit_mode == false) { + client.query("SET jit = off").catch((err: Error) => { + console.warn( + `Failed to set jit=off on ${label} client: ${err.message}` + ); + }); + } }); } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index c3e796fc1..6b6ac95aa 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -184,7 +184,8 @@ export const configSchema = z .number() .positive() .optional() - .default(5000) + .default(5000), + jit_mode: z.boolean().default(true) }) .optional() .prefault({}) From 83de8576bf31d834b08fc53e4ea4354ab816dde9 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 7 Jul 2026 10:59:01 -0400 Subject: [PATCH 6/7] Add ? to support undefined --- server/private/middlewares/verifySubscription.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/server/private/middlewares/verifySubscription.ts b/server/private/middlewares/verifySubscription.ts index a2dce7818..3ae01b338 100644 --- a/server/private/middlewares/verifySubscription.ts +++ b/server/private/middlewares/verifySubscription.ts @@ -17,6 +17,7 @@ import HttpCode from "@server/types/HttpCode"; import { build } from "@server/build"; import { getOrgTierData } from "#private/lib/billing"; import { Tier } from "@server/types/Tiers"; +import logger from "@server/logger"; export function verifyValidSubscription(tiers: Tier[]) { return async function ( @@ -30,9 +31,9 @@ export function verifyValidSubscription(tiers: Tier[]) { } const orgId = - req.params.orgId || - req.body.orgId || - req.query.orgId || + req.params?.orgId || + req.body?.orgId || + req.query?.orgId || req.userOrgId; if (!orgId) { @@ -65,7 +66,7 @@ export function verifyValidSubscription(tiers: Tier[]) { return next(); } catch (e) { - console.error(e); + logger.error(e); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, From 417438c209aad5769d41a5cfbae1b8e0a1f37141 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 7 Jul 2026 14:04:34 -0400 Subject: [PATCH 7/7] Remove feature flags from labels --- server/lib/billing/tierMatrix.ts | 2 - server/private/routers/external.ts | 54 -------- server/private/routers/labels/index.ts | 19 --- server/routers/client/listClients.ts | 38 +++--- server/routers/external.ts | 43 +++++++ .../routers/labels/attachLabelToItem.ts | 13 -- .../routers/labels/createOrgLabel.ts | 12 -- .../routers/labels/deleteOrgLabel.ts | 12 -- .../routers/labels/detachLabelFromItem.ts | 13 -- server/routers/labels/index.ts | 6 + .../routers/labels/listOrgLabels.ts | 13 -- .../routers/labels/updateOrgLabel.ts | 13 -- .../launcher/launcherResourceAccess.ts | 115 +++++------------- server/routers/resource/getUserResources.ts | 85 ++++++------- server/routers/resource/listResources.ts | 74 +++++------ .../resource/listUserResourceAliases.ts | 9 +- server/routers/site/listSites.ts | 71 +++++------ .../siteResource/listAllSiteResourcesByOrg.ts | 38 +++--- .../settings/{(private) => }/labels/page.tsx | 4 - src/components/MachineClientsTable.tsx | 14 +-- src/components/OrgLabelsTable.tsx | 3 - src/components/PrivateResourcesTable.tsx | 14 +-- src/components/PublicResourcesTable.tsx | 14 +-- src/components/SitesTable.tsx | 20 ++- 24 files changed, 233 insertions(+), 466 deletions(-) delete mode 100644 server/private/routers/labels/index.ts rename server/{private => }/routers/labels/attachLabelToItem.ts (94%) rename server/{private => }/routers/labels/createOrgLabel.ts (91%) rename server/{private => }/routers/labels/deleteOrgLabel.ts (82%) rename server/{private => }/routers/labels/detachLabelFromItem.ts (94%) create mode 100644 server/routers/labels/index.ts rename server/{private => }/routers/labels/listOrgLabels.ts (91%) rename server/{private => }/routers/labels/updateOrgLabel.ts (89%) rename src/app/[orgId]/settings/{(private) => }/labels/page.tsx (90%) diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index f0e6dc95a..7c0b591ca 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -23,7 +23,6 @@ export enum TierFeature { StandaloneHealthChecks = "standaloneHealthChecks", AlertingRules = "alertingRules", WildcardSubdomain = "wildcardSubdomain", - Labels = "labels", NewtAutoUpdate = "newtAutoUpdate", ResourcePolicies = "resourcePolicies", AdvancedPublicResources = "advancedPublicResources", @@ -31,7 +30,6 @@ export enum TierFeature { } export const tierMatrix: Record = { - [TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"], diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index c6ee3de55..75a5b8d48 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -31,7 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning"; import * as eventStreamingDestination from "#private/routers/eventStreamingDestination"; import * as alertRule from "#private/routers/alertRule"; import * as healthChecks from "#private/routers/healthChecks"; -import * as labels from "#private/routers/labels"; import * as client from "@server/routers/client"; import * as resource from "#private/routers/resource"; import * as policy from "#private/routers/policy"; @@ -810,59 +809,6 @@ authenticated.get( alertRule.getAlertRule ); -authenticated.get( - "/org/:orgId/labels", - verifyValidLicense, - verifyOrgAccess, - verifyValidSubscription(tierMatrix.labels), - verifyUserHasAction(ActionsEnum.listOrgLabels), - labels.listOrgLabels -); - -authenticated.post( - "/org/:orgId/labels", - verifyValidLicense, - verifyOrgAccess, - verifyValidSubscription(tierMatrix.labels), - verifyUserHasAction(ActionsEnum.createOrgLabel), - labels.createOrgLabel -); - -authenticated.patch( - "/org/:orgId/label/:labelId", - verifyValidLicense, - verifyOrgAccess, - verifyValidSubscription(tierMatrix.labels), - verifyUserHasAction(ActionsEnum.updateOrgLabel), - labels.updateOrgLabel -); - -authenticated.delete( - "/org/:orgId/label/:labelId", - verifyValidLicense, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.deleteOrgLabel), - labels.deleteOrgLabel -); - -authenticated.put( - "/org/:orgId/label/:labelId/attach", - verifyValidLicense, - verifyOrgAccess, - verifyValidSubscription(tierMatrix.labels), - verifyUserHasAction(ActionsEnum.attachLabelToItem), - labels.attachLabelToItem -); - -authenticated.put( - "/org/:orgId/label/:labelId/detach", - verifyValidLicense, - verifyOrgAccess, - verifyValidSubscription(tierMatrix.labels), - verifyUserHasAction(ActionsEnum.detachLabelFromItem), - labels.detachLabelFromItem -); - authenticated.get( "/org/:orgId/health-checks", verifyValidLicense, diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts deleted file mode 100644 index d988d8e38..000000000 --- a/server/private/routers/labels/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - -export * from "./listOrgLabels"; -export * from "./createOrgLabel"; -export * from "./updateOrgLabel"; -export * from "./attachLabelToItem"; -export * from "./detachLabelFromItem"; -export * from "./deleteOrgLabel"; diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 98c0fc550..3a05c41c5 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -304,11 +304,6 @@ export async function listClients( (client) => client.clientId ); - const isLabelFeatureEnabled = await isLicensedOrSubscribed( - orgId, - tierMatrix.labels - ); - // Get client count with filter const conditions = [ and( @@ -341,7 +336,7 @@ export async function listClients( conditions.push(or(...filterAggregates)); } - if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) { + if (labelFilter && labelFilter.length > 0) { conditions.push( inArray( clients.clientId, @@ -361,25 +356,20 @@ export async function listClients( const q = "%" + query.toLowerCase() + "%"; const queryList = [ like(sql`LOWER(${clients.name})`, q), - like(sql`LOWER(${clients.niceId})`, q) + like(sql`LOWER(${clients.niceId})`, q), + inArray( + clients.clientId, + db + .select({ id: clientLabels.clientId }) + .from(clientLabels) + .innerJoin( + labels, + eq(labels.labelId, clientLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) + ) ]; - if (isLabelFeatureEnabled) { - queryList.push( - inArray( - clients.clientId, - db - .select({ id: clientLabels.clientId }) - .from(clientLabels) - .innerJoin( - labels, - eq(labels.labelId, clientLabels.labelId) - ) - .where(like(sql`LOWER(${labels.name})`, q)) - ) - ); - } - conditions.push(or(...queryList)); } @@ -414,7 +404,7 @@ export async function listClients( clientId: number; }> = []; - if (isLabelFeatureEnabled && clientIds.length > 0) { + if (clientIds.length > 0) { labelsForClients = await db .select({ labelId: labels.labelId, diff --git a/server/routers/external.ts b/server/routers/external.ts index 8a22393ca..9e4c5b3f1 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -54,6 +54,7 @@ import { build } from "@server/build"; import { createStore } from "#dynamic/lib/rateLimitStore"; import { logActionAudit } from "#dynamic/middlewares"; import { checkRoundTripMessage } from "./ws"; +import * as labels from "@server/routers/labels"; // Root routes export const unauthenticated = Router(); @@ -1335,6 +1336,48 @@ authenticated.get( authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage); +authenticated.get( + "/org/:orgId/labels", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listOrgLabels), + labels.listOrgLabels +); + +authenticated.post( + "/org/:orgId/labels", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createOrgLabel), + labels.createOrgLabel +); + +authenticated.patch( + "/org/:orgId/label/:labelId", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateOrgLabel), + labels.updateOrgLabel +); + +authenticated.delete( + "/org/:orgId/label/:labelId", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteOrgLabel), + labels.deleteOrgLabel +); + +authenticated.put( + "/org/:orgId/label/:labelId/attach", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.attachLabelToItem), + labels.attachLabelToItem +); + +authenticated.put( + "/org/:orgId/label/:labelId/detach", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.detachLabelFromItem), + labels.detachLabelFromItem +); + // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/routers/labels/attachLabelToItem.ts similarity index 94% rename from server/private/routers/labels/attachLabelToItem.ts rename to server/routers/labels/attachLabelToItem.ts index d011a606d..0d9e49b35 100644 --- a/server/private/routers/labels/attachLabelToItem.ts +++ b/server/routers/labels/attachLabelToItem.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { clients, clientLabels, diff --git a/server/private/routers/labels/createOrgLabel.ts b/server/routers/labels/createOrgLabel.ts similarity index 91% rename from server/private/routers/labels/createOrgLabel.ts rename to server/routers/labels/createOrgLabel.ts index c856eecf4..6fa20816e 100644 --- a/server/private/routers/labels/createOrgLabel.ts +++ b/server/routers/labels/createOrgLabel.ts @@ -1,15 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ import { db, labels, diff --git a/server/private/routers/labels/deleteOrgLabel.ts b/server/routers/labels/deleteOrgLabel.ts similarity index 82% rename from server/private/routers/labels/deleteOrgLabel.ts rename to server/routers/labels/deleteOrgLabel.ts index f091c910a..c46e67437 100644 --- a/server/private/routers/labels/deleteOrgLabel.ts +++ b/server/routers/labels/deleteOrgLabel.ts @@ -1,15 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ import { db, labels } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; diff --git a/server/private/routers/labels/detachLabelFromItem.ts b/server/routers/labels/detachLabelFromItem.ts similarity index 94% rename from server/private/routers/labels/detachLabelFromItem.ts rename to server/routers/labels/detachLabelFromItem.ts index 9a5545312..52c752ca8 100644 --- a/server/private/routers/labels/detachLabelFromItem.ts +++ b/server/routers/labels/detachLabelFromItem.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { clients, clientLabels, diff --git a/server/routers/labels/index.ts b/server/routers/labels/index.ts new file mode 100644 index 000000000..aa8c34222 --- /dev/null +++ b/server/routers/labels/index.ts @@ -0,0 +1,6 @@ +export * from "./listOrgLabels"; +export * from "./createOrgLabel"; +export * from "./updateOrgLabel"; +export * from "./attachLabelToItem"; +export * from "./detachLabelFromItem"; +export * from "./deleteOrgLabel"; diff --git a/server/private/routers/labels/listOrgLabels.ts b/server/routers/labels/listOrgLabels.ts similarity index 91% rename from server/private/routers/labels/listOrgLabels.ts rename to server/routers/labels/listOrgLabels.ts index dc2b50017..6d6b853cf 100644 --- a/server/private/routers/labels/listOrgLabels.ts +++ b/server/routers/labels/listOrgLabels.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { db, labels } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; diff --git a/server/private/routers/labels/updateOrgLabel.ts b/server/routers/labels/updateOrgLabel.ts similarity index 89% rename from server/private/routers/labels/updateOrgLabel.ts rename to server/routers/labels/updateOrgLabel.ts index 134a01f02..fe8a6f573 100644 --- a/server/private/routers/labels/updateOrgLabel.ts +++ b/server/routers/labels/updateOrgLabel.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { db, labels } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; diff --git a/server/routers/launcher/launcherResourceAccess.ts b/server/routers/launcher/launcherResourceAccess.ts index 246f86688..ec4500262 100644 --- a/server/routers/launcher/launcherResourceAccess.ts +++ b/server/routers/launcher/launcherResourceAccess.ts @@ -274,10 +274,7 @@ function combineOrConditions( return or(...parts); } -function buildSearchConditionForPublic( - query: string, - labelsFeatureEnabled: boolean -) { +function buildSearchConditionForPublic(query: string) { if (!query.trim()) { return undefined; } @@ -295,32 +292,21 @@ function buildSearchConditionForPublic( .leftJoin(sites, eq(targets.siteId, sites.siteId)) .leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId)) .where(like(sql`LOWER(${exitNodes.endpoint})`, pattern)) + ), + inArray( + resources.resourceId, + db + .select({ id: resourceLabels.resourceId }) + .from(resourceLabels) + .innerJoin(labels, eq(labels.labelId, resourceLabels.labelId)) + .where(like(sql`LOWER(${labels.name})`, pattern)) ) ]; - if (labelsFeatureEnabled) { - queryList.push( - inArray( - resources.resourceId, - db - .select({ id: resourceLabels.resourceId }) - .from(resourceLabels) - .innerJoin( - labels, - eq(labels.labelId, resourceLabels.labelId) - ) - .where(like(sql`LOWER(${labels.name})`, pattern)) - ) - ); - } - return or(...queryList); } -function buildSearchConditionForSiteResource( - query: string, - labelsFeatureEnabled: boolean -) { +function buildSearchConditionForSiteResource(query: string) { if (!query.trim()) { return undefined; } @@ -335,40 +321,34 @@ function buildSearchConditionForSiteResource( like(sql`LOWER(${siteResources.scheme})`, pattern), like(sql`LOWER(${siteResources.alias})`, pattern), like(sql`LOWER(${siteResources.fullDomain})`, pattern), - like(sql`LOWER(${siteResources.aliasAddress})`, pattern) + like(sql`LOWER(${siteResources.aliasAddress})`, pattern), + inArray( + siteResources.siteResourceId, + db + .select({ id: siteResourceLabels.siteResourceId }) + .from(siteResourceLabels) + .innerJoin( + labels, + eq(labels.labelId, siteResourceLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, pattern)) + ) ]; - if (labelsFeatureEnabled) { - queryList.push( - inArray( - siteResources.siteResourceId, - db - .select({ id: siteResourceLabels.siteResourceId }) - .from(siteResourceLabels) - .innerJoin( - labels, - eq(labels.labelId, siteResourceLabels.labelId) - ) - .where(like(sql`LOWER(${labels.name})`, pattern)) - ) - ); - } - return or(...queryList); } async function filterPublicResourceIdsByTextSearch( orgId: string, resourceIds: number[], - query: string, - labelsFeatureEnabled: boolean + query: string ): Promise { if (!query.trim() || resourceIds.length === 0) { return resourceIds; } const textMatch = combineOrConditions( - buildSearchConditionForPublic(query, labelsFeatureEnabled), + buildSearchConditionForPublic(query), buildSiteNameSearchCondition(query) ); if (!textMatch) { @@ -395,15 +375,14 @@ async function filterPublicResourceIdsByTextSearch( async function filterSiteResourceIdsByTextSearch( orgId: string, siteResourceIds: number[], - query: string, - labelsFeatureEnabled: boolean + query: string ): Promise { if (!query.trim() || siteResourceIds.length === 0) { return siteResourceIds; } const textMatch = combineOrConditions( - buildSearchConditionForSiteResource(query, labelsFeatureEnabled), + buildSearchConditionForSiteResource(query), buildSiteNameSearchCondition(query) ); if (!textMatch) { @@ -430,10 +409,6 @@ async function filterSiteResourceIdsByTextSearch( return rows.map((row) => row.siteResourceId); } -async function labelsEnabled(orgId: string): Promise { - return isLicensedOrSubscribed(orgId, tierMatrix.labels); -} - async function fetchLabelsForResources( orgId: string, resourceIds: number[], @@ -445,10 +420,6 @@ async function fetchLabelsForResources( const byResourceId = new Map(); const bySiteResourceId = new Map(); - if (!(await labelsEnabled(orgId))) { - return { byResourceId, bySiteResourceId }; - } - const [resourceLabelRows, siteResourceLabelRows] = await Promise.all([ resourceIds.length === 0 ? Promise.resolve([]) @@ -524,15 +495,8 @@ async function listSiteGroups( ): Promise<{ groups: LauncherGroup[]; total: number }> { const siteFilterIds = parseIdListParam(query.siteIds); const labelFilterIds = parseIdListParam(query.labelIds); - const labelsFeatureEnabled = await labelsEnabled(orgId); - const searchPublic = buildSearchConditionForPublic( - query.query, - labelsFeatureEnabled - ); - const searchSite = buildSearchConditionForSiteResource( - query.query, - labelsFeatureEnabled - ); + const searchPublic = buildSearchConditionForPublic(query.query); + const searchSite = buildSearchConditionForSiteResource(query.query); const siteCountMap = new Map(); if (accessible.resourceIds.length > 0) { @@ -775,10 +739,6 @@ async function listLabelGroups( >(); let unlabeledCount = 0; - if (!(await labelsEnabled(orgId))) { - return { groups: [], total: 0 }; - } - const matchesLabelFilters = (labelId: number) => labelFilterIds.length === 0 || labelFilterIds.includes(labelId); @@ -788,7 +748,7 @@ async function listLabelGroups( eq(resources.orgId, orgId), eq(resources.enabled, true) ]; - const searchPublic = buildSearchConditionForPublic(query.query, true); + const searchPublic = buildSearchConditionForPublic(query.query); if (searchPublic) { publicConditions.push(searchPublic); } @@ -852,10 +812,7 @@ async function listLabelGroups( eq(siteResources.orgId, orgId), eq(siteResources.enabled, true) ]; - const searchSite = buildSearchConditionForSiteResource( - query.query, - true - ); + const searchSite = buildSearchConditionForSiteResource(query.query); if (searchSite) { siteConditions.push(searchSite); } @@ -1302,23 +1259,19 @@ async function listLauncherResourcesForUserUncached( } } - const labelsFeatureEnabled = await labelsEnabled(orgId); - if (query.query.trim()) { if (filteredResourceIds.length > 0) { filteredResourceIds = await filterPublicResourceIdsByTextSearch( orgId, filteredResourceIds, - query.query, - labelsFeatureEnabled + query.query ); } if (filteredSiteResourceIds.length > 0) { filteredSiteResourceIds = await filterSiteResourceIdsByTextSearch( orgId, filteredSiteResourceIds, - query.query, - labelsFeatureEnabled + query.query ); } } @@ -1518,10 +1471,6 @@ async function collectAccessibleLabels( ): Promise> { const labelMap = new Map(); - if (!(await labelsEnabled(orgId))) { - return labelMap; - } - if (accessible.resourceIds.length > 0) { const publicConditions = [ inArray(resources.resourceId, accessible.resourceIds), diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index c82dcac48..92951ee1d 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -363,11 +363,6 @@ export async function getUserResources( (r) => r.siteResourceId ); - const isLabelFeatureEnabled = await isLicensedOrSubscribed( - orgId, - tierMatrix.labels - ); - let labelsForResources: Array<{ labelId: number; name: string; @@ -381,49 +376,45 @@ export async function getUserResources( siteResourceId: number; }> = []; - if (isLabelFeatureEnabled) { - [labelsForResources, labelsForSiteResources] = await Promise.all([ - resourceIdList.length === 0 - ? Promise.resolve([]) - : db - .select({ - labelId: labels.labelId, - name: labels.name, - color: labels.color, - resourceId: resourceLabels.resourceId - }) - .from(labels) - .innerJoin( - resourceLabels, - eq(resourceLabels.labelId, labels.labelId) + [labelsForResources, labelsForSiteResources] = await Promise.all([ + resourceIdList.length === 0 + ? Promise.resolve([]) + : db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + resourceId: resourceLabels.resourceId + }) + .from(labels) + .innerJoin( + resourceLabels, + eq(resourceLabels.labelId, labels.labelId) + ) + .where(inArray(resourceLabels.resourceId, resourceIdList)) + .orderBy(asc(resourceLabels.resourceLabelId)), + siteResourceIdList.length === 0 + ? Promise.resolve([]) + : db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + siteResourceId: siteResourceLabels.siteResourceId + }) + .from(labels) + .innerJoin( + siteResourceLabels, + eq(siteResourceLabels.labelId, labels.labelId) + ) + .where( + inArray( + siteResourceLabels.siteResourceId, + siteResourceIdList ) - .where( - inArray(resourceLabels.resourceId, resourceIdList) - ) - .orderBy(asc(resourceLabels.resourceLabelId)), - siteResourceIdList.length === 0 - ? Promise.resolve([]) - : db - .select({ - labelId: labels.labelId, - name: labels.name, - color: labels.color, - siteResourceId: siteResourceLabels.siteResourceId - }) - .from(labels) - .innerJoin( - siteResourceLabels, - eq(siteResourceLabels.labelId, labels.labelId) - ) - .where( - inArray( - siteResourceLabels.siteResourceId, - siteResourceIdList - ) - ) - .orderBy(asc(siteResourceLabels.siteResourceLabelId)) - ]); - } + ) + .orderBy(asc(siteResourceLabels.siteResourceLabelId)) + ]); // Check for password, pincode, and whitelist protection for each resource const resourcesWithAuth = await Promise.all( diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index d1deb9d39..9567b5bcd 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -484,11 +484,6 @@ export async function listResources( ); } - const isLabelFeatureEnabled = await isLicensedOrSubscribed( - orgId, - tierMatrix.labels - ); - let accessibleResources: Array<{ resourceId: number }>; if (req.user) { accessibleResources = await db @@ -676,7 +671,7 @@ export async function listResources( ); } - if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) { + if (labelFilter && labelFilter.length > 0) { conditions.push( inArray( resources.resourceId, @@ -697,25 +692,20 @@ export async function listResources( const queryList = [ like(sql`LOWER(${resources.name})`, q), like(sql`LOWER(${resources.niceId})`, q), - like(sql`LOWER(${resources.fullDomain})`, q) + like(sql`LOWER(${resources.fullDomain})`, q), + inArray( + resources.resourceId, + db + .select({ id: resourceLabels.resourceId }) + .from(resourceLabels) + .innerJoin( + labels, + eq(labels.labelId, resourceLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) + ) ]; - if (isLabelFeatureEnabled) { - queryList.push( - inArray( - resources.resourceId, - db - .select({ id: resourceLabels.resourceId }) - .from(resourceLabels) - .innerJoin( - labels, - eq(labels.labelId, resourceLabels.labelId) - ) - .where(like(sql`LOWER(${labels.name})`, q)) - ) - ); - } - conditions.push(or(...queryList)); } @@ -747,27 +737,23 @@ export async function listResources( resourceId: number; }> = []; - if (isLabelFeatureEnabled) { - labelsForResources = - resourceIdList.length === 0 - ? [] - : await db - .select({ - labelId: labels.labelId, - name: labels.name, - color: labels.color, - resourceId: resourceLabels.resourceId - }) - .from(labels) - .innerJoin( - resourceLabels, - eq(resourceLabels.labelId, labels.labelId) - ) - .where( - inArray(resourceLabels.resourceId, resourceIdList) - ) - .orderBy(asc(resourceLabels.resourceLabelId)); - } + labelsForResources = + resourceIdList.length === 0 + ? [] + : await db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + resourceId: resourceLabels.resourceId + }) + .from(labels) + .innerJoin( + resourceLabels, + eq(resourceLabels.labelId, labels.labelId) + ) + .where(inArray(resourceLabels.resourceId, resourceIdList)) + .orderBy(asc(resourceLabels.resourceLabelId)); const allResourceTargets = resourceIdList.length === 0 diff --git a/server/routers/resource/listUserResourceAliases.ts b/server/routers/resource/listUserResourceAliases.ts index dab8afc23..4ec87b8bb 100644 --- a/server/routers/resource/listUserResourceAliases.ts +++ b/server/routers/resource/listUserResourceAliases.ts @@ -248,11 +248,6 @@ export async function listUserResourceAliases( }); } - const isLabelFeatureEnabled = await isLicensedOrSubscribed( - orgId, - tierMatrix.labels - ); - const whereConditions = [ eq(siteResources.orgId, orgId), eq(siteResources.enabled, true), @@ -262,7 +257,7 @@ export async function listUserResourceAliases( inArray(siteResources.siteResourceId, accessibleSiteResourceIds) ]; - if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) { + if (labelFilter && labelFilter.length > 0) { whereConditions.push( inArray( siteResources.siteResourceId, @@ -310,7 +305,7 @@ export async function listUserResourceAliases( siteResourceId: number; }> = []; - if (isLabelFeatureEnabled && siteResourceIdList.length > 0) { + if (siteResourceIdList.length > 0) { labelsForSiteResources = await db .select({ name: labels.name, diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 86c555f93..1f8e41f26 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -235,11 +235,6 @@ export async function listSites( ); } - const isLabelFeatureEnabled = await isLicensedOrSubscribed( - orgId, - tierMatrix.labels - ); - const { pageSize, page, @@ -291,7 +286,7 @@ export async function listSites( conditions.push(eq(sites.status, status)); } - if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) { + if (labelFilter && labelFilter.length > 0) { conditions.push( inArray( sites.siteId, @@ -311,24 +306,20 @@ export async function listSites( const q = "%" + query.toLowerCase() + "%"; const queryList = [ like(sql`LOWER(${sites.name})`, q), - like(sql`LOWER(${sites.niceId})`, q) + like(sql`LOWER(${sites.niceId})`, q), + inArray( + sites.siteId, + db + .select({ id: siteLabels.siteId }) + .from(siteLabels) + .innerJoin( + labels, + eq(labels.labelId, siteLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) + ) ]; - if (isLabelFeatureEnabled) { - queryList.push( - inArray( - sites.siteId, - db - .select({ id: siteLabels.siteId }) - .from(siteLabels) - .innerJoin( - labels, - eq(labels.labelId, siteLabels.labelId) - ) - .where(like(sql`LOWER(${labels.name})`, q)) - ) - ); - } conditions.push(or(...queryList)!); } @@ -366,25 +357,23 @@ export async function listSites( siteId: number; }> = []; - if (isLabelFeatureEnabled) { - labelsForSites = - siteIds.length === 0 - ? [] - : await db - .select({ - labelId: labels.labelId, - name: labels.name, - color: labels.color, - siteId: siteLabels.siteId - }) - .from(labels) - .innerJoin( - siteLabels, - eq(siteLabels.labelId, labels.labelId) - ) - .where(inArray(siteLabels.siteId, siteIds)) - .orderBy(asc(siteLabels.siteLabelId)); - } + labelsForSites = + siteIds.length === 0 + ? [] + : await db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + siteId: siteLabels.siteId + }) + .from(labels) + .innerJoin( + siteLabels, + eq(siteLabels.labelId, labels.labelId) + ) + .where(inArray(siteLabels.siteId, siteIds)) + .orderBy(asc(siteLabels.siteLabelId)); const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => { const siteWithUpdate: SiteWithUpdateAvailable = { ...site }; diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 721d76bf6..4515e033e 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -286,11 +286,6 @@ export async function listAllSiteResourcesByOrg( labels: labelFilter } = parsedQuery.data; - const isLabelFeatureEnabled = await isLicensedOrSubscribed( - orgId, - tierMatrix.labels - ); - const conditions = [and(eq(siteResources.orgId, orgId))]; if (siteId != null) { @@ -320,7 +315,7 @@ export async function listAllSiteResourcesByOrg( conditions.push(eq(siteResources.mode, mode)); } - if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) { + if (labelFilter && labelFilter.length > 0) { conditions.push( inArray( siteResources.siteResourceId, @@ -344,25 +339,20 @@ export async function listAllSiteResourcesByOrg( like(sql`LOWER(${siteResources.destination})`, q), like(sql`LOWER(${siteResources.alias})`, q), like(sql`LOWER(${siteResources.aliasAddress})`, q), - like(sql`LOWER(${sites.name})`, q) + like(sql`LOWER(${sites.name})`, q), + inArray( + siteResources.siteResourceId, + db + .select({ id: siteResourceLabels.siteResourceId }) + .from(siteResourceLabels) + .innerJoin( + labels, + eq(labels.labelId, siteResourceLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) + ) ]; - if (isLabelFeatureEnabled) { - queryList.push( - inArray( - siteResources.siteResourceId, - db - .select({ id: siteResourceLabels.siteResourceId }) - .from(siteResourceLabels) - .innerJoin( - labels, - eq(labels.labelId, siteResourceLabels.labelId) - ) - .where(like(sql`LOWER(${labels.name})`, q)) - ) - ); - } - conditions.push(or(...queryList)); } @@ -403,7 +393,7 @@ export async function listAllSiteResourcesByOrg( siteResourceId: number; }> = []; - if (isLabelFeatureEnabled && siteResourceIdList.length > 0) { + if (siteResourceIdList.length > 0) { labelsForSiteResources = await db .select({ labelId: labels.labelId, diff --git a/src/app/[orgId]/settings/(private)/labels/page.tsx b/src/app/[orgId]/settings/labels/page.tsx similarity index 90% rename from src/app/[orgId]/settings/(private)/labels/page.tsx rename to src/app/[orgId]/settings/labels/page.tsx index f2ef0aa44..806c60325 100644 --- a/src/app/[orgId]/settings/(private)/labels/page.tsx +++ b/src/app/[orgId]/settings/labels/page.tsx @@ -3,9 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { ListOrgLabelsResponse } from "@server/routers/labels/types"; import { AxiosResponse } from "axios"; import OrgLabelsTable from "@app/components/OrgLabelsTable"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; import type { Metadata } from "next"; import { getTranslations } from "next-intl/server"; @@ -51,8 +49,6 @@ export default async function LabelsPage({ params, searchParams }: Props) { description={t("orgLabelsDescription")} /> - - {t("address")} - } - ]; - - if (isLabelFeatureEnabled) { - baseColumns.push({ + }, + { id: "labels", accessorKey: "labels", header: () => ( @@ -458,8 +454,8 @@ export default function MachineClientsTable({ orgId={orgId} /> ) - }); - } + } + ]; // Only include actions column if there are rows without userIds if (hasRowsWithoutUserId) { @@ -541,7 +537,7 @@ export default function MachineClientsTable({ } return baseColumns; - }, [hasRowsWithoutUserId, isLabelFeatureEnabled, orgId, t, searchParams]); + }, [hasRowsWithoutUserId, orgId, t, searchParams]); function handleFilterChange( column: string, diff --git a/src/components/OrgLabelsTable.tsx b/src/components/OrgLabelsTable.tsx index 3b2ea691b..1d936f274 100644 --- a/src/components/OrgLabelsTable.tsx +++ b/src/components/OrgLabelsTable.tsx @@ -21,9 +21,6 @@ import { ControlledDataTable, type ExtendedColumnDef } from "./ui/controlled-data-table"; -import { LabelBadge } from "./label-badge"; -import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; -import { cn } from "@app/lib/cn"; import ConfirmDeleteDialog from "./ConfirmDeleteDialog"; import { CreateOrgLabelDialog } from "./CreateOrgLabelDialog"; import { EditOrgLabelDialog } from "./EditOrgLabelDialog"; diff --git a/src/components/PrivateResourcesTable.tsx b/src/components/PrivateResourcesTable.tsx index ff854a1a8..9d0de1036 100644 --- a/src/components/PrivateResourcesTable.tsx +++ b/src/components/PrivateResourcesTable.tsx @@ -151,7 +151,6 @@ export default function PrivateResourcesTable({ const [isRefreshing, startRefreshTransition] = useTransition(); const { isPaidUser } = usePaidStatus(); - const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); // useEffect(() => { // const interval = setInterval(() => { @@ -476,11 +475,8 @@ export default function PrivateResourcesTable({ ); } - } - ]; - - if (isLabelFeatureEnabled) { - cols.splice(cols.length - 1, 0, { + }, + { id: "labels", accessorKey: "labels", header: () => ( @@ -500,11 +496,11 @@ export default function PrivateResourcesTable({ orgId={orgId} /> ) - }); - } + } + ]; return cols; - }, [isLabelFeatureEnabled, orgId, t, searchParams]); + }, [orgId, t, searchParams]); function handleFilterChange( column: string, diff --git a/src/components/PublicResourcesTable.tsx b/src/components/PublicResourcesTable.tsx index 1cc13ad80..2ac60013a 100644 --- a/src/components/PublicResourcesTable.tsx +++ b/src/components/PublicResourcesTable.tsx @@ -151,7 +151,6 @@ export default function PublicResourcesTable({ useState(); const { isPaidUser } = usePaidStatus(); - const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); const [isRefreshing, startTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); @@ -604,11 +603,8 @@ export default function PublicResourcesTable({ ); } - } - ]; - - if (isLabelFeatureEnabled) { - cols.splice(cols.length - 1, 0, { + }, + { id: "labels", accessorKey: "labels", header: () => ( @@ -625,11 +621,11 @@ export default function PublicResourcesTable({ cell: ({ row }: { row: { original: ResourceRow } }) => ( ) - }); - } + } + ]; return cols; - }, [isLabelFeatureEnabled, orgId, t, searchParams]); + }, [orgId, t, searchParams]); function handleFilterChange( column: string, diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index efcf83e72..2592a2143 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -53,7 +53,6 @@ import { import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { LabelColumnFilterButton } from "./LabelColumnFilterButton"; import { LabelsTableCell } from "./LabelsTableCell"; import { useQuery } from "@tanstack/react-query"; @@ -114,7 +113,6 @@ export default function SitesTable({ const [isNavigatingToAddPage, startNavigation] = useTransition(); const { isPaidUser } = usePaidStatus(); - const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); const api = createApiClient(useEnvContext()); const t = useTranslations(); @@ -171,7 +169,10 @@ export default function SitesTable({ toast({ variant: "destructive", title: t("siteErrorRestart"), - description: formatAxiosError(e, t("siteErrorRestartDescription")) + description: formatAxiosError( + e, + t("siteErrorRestartDescription") + ) }); } finally { setRestartingSite(null); @@ -598,11 +599,8 @@ export default function SitesTable({ ); } - } - ]; - - if (isLabelFeatureEnabled) { - cols.splice(cols.length - 1, 0, { + }, + { accessorKey: "labels", header: () => ( ( ) - }); - } + } + ]; return cols; - }, [isLabelFeatureEnabled, orgId, t, searchParams, latestNewtVersion]); + }, [orgId, t, searchParams, latestNewtVersion]); function toggleSort(column: string) { const newSearch = getNextSortOrder(column, searchParams);