mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-08 15:14:52 +02:00
Merge branch 'dev' into private-resource-page
This commit is contained in:
+24
-11
@@ -1,3 +1,4 @@
|
|||||||
|
import config from "@server/lib/config";
|
||||||
import { Pool, PoolConfig } from "pg";
|
import { Pool, PoolConfig } from "pg";
|
||||||
|
|
||||||
export function createPoolConfig(
|
export function createPoolConfig(
|
||||||
@@ -18,17 +19,7 @@ export function createPoolConfig(
|
|||||||
keepAliveInitialDelayMillis: 10000, // send first keepalive after 10s of idle
|
keepAliveInitialDelayMillis: 10000, // send first keepalive after 10s of idle
|
||||||
// Allow connections to be released and recreated more aggressively
|
// Allow connections to be released and recreated more aggressively
|
||||||
// to avoid stale connections building up
|
// to avoid stale connections building up
|
||||||
allowExitOnIdle: false,
|
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"
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +40,28 @@ export function attachPoolErrorHandlers(pool: Pool, label: string): void {
|
|||||||
`Failed to set statement_timeout on ${label} client: ${err.message}`
|
`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}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ export enum TierFeature {
|
|||||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||||
AlertingRules = "alertingRules",
|
AlertingRules = "alertingRules",
|
||||||
WildcardSubdomain = "wildcardSubdomain",
|
WildcardSubdomain = "wildcardSubdomain",
|
||||||
Labels = "labels",
|
|
||||||
NewtAutoUpdate = "newtAutoUpdate",
|
NewtAutoUpdate = "newtAutoUpdate",
|
||||||
ResourcePolicies = "resourcePolicies",
|
ResourcePolicies = "resourcePolicies",
|
||||||
AdvancedPublicResources = "advancedPublicResources",
|
AdvancedPublicResources = "advancedPublicResources",
|
||||||
@@ -31,7 +30,6 @@ export enum TierFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
[TierFeature.Labels]: ["tier1", "tier2", "tier3", "enterprise"],
|
|
||||||
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.OrgOidc]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.LoginPageDomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
[TierFeature.DeviceApprovals]: ["tier1", "tier3", "enterprise"],
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export async function applyNewtDockerBlueprint(
|
|||||||
source: "NEWT"
|
source: "NEWT"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update database from config: ${error}`);
|
logger.debug(`Failed to update database from config: ${error}`);
|
||||||
await sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: "newt/blueprint/results",
|
type: "newt/blueprint/results",
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ export const configSchema = z
|
|||||||
.number()
|
.number()
|
||||||
.positive()
|
.positive()
|
||||||
.optional()
|
.optional()
|
||||||
.default(5000)
|
.default(5000),
|
||||||
|
jit_mode: z.boolean().default(true)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.prefault({})
|
.prefault({})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { Tier } from "@server/types/Tiers";
|
import { Tier } from "@server/types/Tiers";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export function verifyValidSubscription(tiers: Tier[]) {
|
export function verifyValidSubscription(tiers: Tier[]) {
|
||||||
return async function (
|
return async function (
|
||||||
@@ -30,9 +31,9 @@ export function verifyValidSubscription(tiers: Tier[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const orgId =
|
const orgId =
|
||||||
req.params.orgId ||
|
req.params?.orgId ||
|
||||||
req.body.orgId ||
|
req.body?.orgId ||
|
||||||
req.query.orgId ||
|
req.query?.orgId ||
|
||||||
req.userOrgId;
|
req.userOrgId;
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
@@ -65,6 +66,7 @@ export function verifyValidSubscription(tiers: Tier[]) {
|
|||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import * as siteProvisioning from "#private/routers/siteProvisioning";
|
|||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
import * as alertRule from "#private/routers/alertRule";
|
import * as alertRule from "#private/routers/alertRule";
|
||||||
import * as healthChecks from "#private/routers/healthChecks";
|
import * as healthChecks from "#private/routers/healthChecks";
|
||||||
import * as labels from "#private/routers/labels";
|
|
||||||
import * as client from "@server/routers/client";
|
import * as client from "@server/routers/client";
|
||||||
import * as resource from "#private/routers/resource";
|
import * as resource from "#private/routers/resource";
|
||||||
import * as policy from "#private/routers/policy";
|
import * as policy from "#private/routers/policy";
|
||||||
@@ -810,59 +809,6 @@ authenticated.get(
|
|||||||
alertRule.getAlertRule
|
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(
|
authenticated.get(
|
||||||
"/org/:orgId/health-checks",
|
"/org/:orgId/health-checks",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
|
|||||||
@@ -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";
|
|
||||||
@@ -99,7 +99,7 @@ export async function applyJSONBlueprint(
|
|||||||
source: "API"
|
source: "API"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update database from config: ${error}`);
|
logger.debug(`Failed to update database from config: ${error}`);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
|
|||||||
@@ -304,11 +304,6 @@ export async function listClients(
|
|||||||
(client) => client.clientId
|
(client) => client.clientId
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.labels
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get client count with filter
|
// Get client count with filter
|
||||||
const conditions = [
|
const conditions = [
|
||||||
and(
|
and(
|
||||||
@@ -341,7 +336,7 @@ export async function listClients(
|
|||||||
conditions.push(or(...filterAggregates));
|
conditions.push(or(...filterAggregates));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
if (labelFilter && labelFilter.length > 0) {
|
||||||
conditions.push(
|
conditions.push(
|
||||||
inArray(
|
inArray(
|
||||||
clients.clientId,
|
clients.clientId,
|
||||||
@@ -361,25 +356,20 @@ export async function listClients(
|
|||||||
const q = "%" + query.toLowerCase() + "%";
|
const q = "%" + query.toLowerCase() + "%";
|
||||||
const queryList = [
|
const queryList = [
|
||||||
like(sql`LOWER(${clients.name})`, q),
|
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));
|
conditions.push(or(...queryList));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +404,7 @@ export async function listClients(
|
|||||||
clientId: number;
|
clientId: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && clientIds.length > 0) {
|
if (clientIds.length > 0) {
|
||||||
labelsForClients = await db
|
labelsForClients = await db
|
||||||
.select({
|
.select({
|
||||||
labelId: labels.labelId,
|
labelId: labels.labelId,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import { build } from "@server/build";
|
|||||||
import { createStore } from "#dynamic/lib/rateLimitStore";
|
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||||
import { logActionAudit } from "#dynamic/middlewares";
|
import { logActionAudit } from "#dynamic/middlewares";
|
||||||
import { checkRoundTripMessage } from "./ws";
|
import { checkRoundTripMessage } from "./ws";
|
||||||
|
import * as labels from "@server/routers/labels";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
export const unauthenticated = Router();
|
export const unauthenticated = Router();
|
||||||
@@ -1335,6 +1336,48 @@ authenticated.get(
|
|||||||
|
|
||||||
authenticated.get("/ws/round-trip-message/:messageId", checkRoundTripMessage);
|
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
|
// Auth routes
|
||||||
export const authRouter = Router();
|
export const authRouter = Router();
|
||||||
unauthenticated.use("/auth", authRouter);
|
unauthenticated.use("/auth", authRouter);
|
||||||
|
|||||||
-13
@@ -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 {
|
import {
|
||||||
clients,
|
clients,
|
||||||
clientLabels,
|
clientLabels,
|
||||||
-12
@@ -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 {
|
import {
|
||||||
db,
|
db,
|
||||||
labels,
|
labels,
|
||||||
-12
@@ -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 { db, labels } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
-13
@@ -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 {
|
import {
|
||||||
clients,
|
clients,
|
||||||
clientLabels,
|
clientLabels,
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export * from "./listOrgLabels";
|
||||||
|
export * from "./createOrgLabel";
|
||||||
|
export * from "./updateOrgLabel";
|
||||||
|
export * from "./attachLabelToItem";
|
||||||
|
export * from "./detachLabelFromItem";
|
||||||
|
export * from "./deleteOrgLabel";
|
||||||
-13
@@ -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 { db, labels } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
-13
@@ -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 { db, labels } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
@@ -321,10 +321,7 @@ function combineOrConditions(
|
|||||||
return or(...parts);
|
return or(...parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSearchConditionForPublic(
|
function buildSearchConditionForPublic(query: string) {
|
||||||
query: string,
|
|
||||||
labelsFeatureEnabled: boolean
|
|
||||||
) {
|
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -342,32 +339,21 @@ function buildSearchConditionForPublic(
|
|||||||
.leftJoin(sites, eq(targets.siteId, sites.siteId))
|
.leftJoin(sites, eq(targets.siteId, sites.siteId))
|
||||||
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
|
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
|
||||||
.where(like(sql`LOWER(${exitNodes.endpoint})`, pattern))
|
.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);
|
return or(...queryList);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSearchConditionForSiteResource(
|
function buildSearchConditionForSiteResource(query: string) {
|
||||||
query: string,
|
|
||||||
labelsFeatureEnabled: boolean
|
|
||||||
) {
|
|
||||||
if (!query.trim()) {
|
if (!query.trim()) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -382,40 +368,34 @@ function buildSearchConditionForSiteResource(
|
|||||||
like(sql`LOWER(${siteResources.scheme})`, pattern),
|
like(sql`LOWER(${siteResources.scheme})`, pattern),
|
||||||
like(sql`LOWER(${siteResources.alias})`, pattern),
|
like(sql`LOWER(${siteResources.alias})`, pattern),
|
||||||
like(sql`LOWER(${siteResources.fullDomain})`, 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);
|
return or(...queryList);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function filterPublicResourceIdsByTextSearch(
|
async function filterPublicResourceIdsByTextSearch(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
resourceIds: number[],
|
resourceIds: number[],
|
||||||
query: string,
|
query: string
|
||||||
labelsFeatureEnabled: boolean
|
|
||||||
): Promise<number[]> {
|
): Promise<number[]> {
|
||||||
if (!query.trim() || resourceIds.length === 0) {
|
if (!query.trim() || resourceIds.length === 0) {
|
||||||
return resourceIds;
|
return resourceIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textMatch = combineOrConditions(
|
const textMatch = combineOrConditions(
|
||||||
buildSearchConditionForPublic(query, labelsFeatureEnabled),
|
buildSearchConditionForPublic(query),
|
||||||
buildSiteNameSearchCondition(query)
|
buildSiteNameSearchCondition(query)
|
||||||
);
|
);
|
||||||
if (!textMatch) {
|
if (!textMatch) {
|
||||||
@@ -442,15 +422,14 @@ async function filterPublicResourceIdsByTextSearch(
|
|||||||
async function filterSiteResourceIdsByTextSearch(
|
async function filterSiteResourceIdsByTextSearch(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
siteResourceIds: number[],
|
siteResourceIds: number[],
|
||||||
query: string,
|
query: string
|
||||||
labelsFeatureEnabled: boolean
|
|
||||||
): Promise<number[]> {
|
): Promise<number[]> {
|
||||||
if (!query.trim() || siteResourceIds.length === 0) {
|
if (!query.trim() || siteResourceIds.length === 0) {
|
||||||
return siteResourceIds;
|
return siteResourceIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textMatch = combineOrConditions(
|
const textMatch = combineOrConditions(
|
||||||
buildSearchConditionForSiteResource(query, labelsFeatureEnabled),
|
buildSearchConditionForSiteResource(query),
|
||||||
buildSiteNameSearchCondition(query)
|
buildSiteNameSearchCondition(query)
|
||||||
);
|
);
|
||||||
if (!textMatch) {
|
if (!textMatch) {
|
||||||
@@ -477,10 +456,6 @@ async function filterSiteResourceIdsByTextSearch(
|
|||||||
return rows.map((row) => row.siteResourceId);
|
return rows.map((row) => row.siteResourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function labelsEnabled(orgId: string): Promise<boolean> {
|
|
||||||
return isLicensedOrSubscribed(orgId, tierMatrix.labels);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLabelsForResources(
|
async function fetchLabelsForResources(
|
||||||
orgId: string,
|
orgId: string,
|
||||||
resourceIds: number[],
|
resourceIds: number[],
|
||||||
@@ -492,10 +467,6 @@ async function fetchLabelsForResources(
|
|||||||
const byResourceId = new Map<number, LauncherLabel[]>();
|
const byResourceId = new Map<number, LauncherLabel[]>();
|
||||||
const bySiteResourceId = new Map<number, LauncherLabel[]>();
|
const bySiteResourceId = new Map<number, LauncherLabel[]>();
|
||||||
|
|
||||||
if (!(await labelsEnabled(orgId))) {
|
|
||||||
return { byResourceId, bySiteResourceId };
|
|
||||||
}
|
|
||||||
|
|
||||||
const [resourceLabelRows, siteResourceLabelRows] = await Promise.all([
|
const [resourceLabelRows, siteResourceLabelRows] = await Promise.all([
|
||||||
resourceIds.length === 0
|
resourceIds.length === 0
|
||||||
? Promise.resolve([])
|
? Promise.resolve([])
|
||||||
@@ -571,15 +542,8 @@ async function listSiteGroups(
|
|||||||
): Promise<{ groups: LauncherGroup[]; total: number }> {
|
): Promise<{ groups: LauncherGroup[]; total: number }> {
|
||||||
const siteFilterIds = parseIdListParam(query.siteIds);
|
const siteFilterIds = parseIdListParam(query.siteIds);
|
||||||
const labelFilterIds = parseIdListParam(query.labelIds);
|
const labelFilterIds = parseIdListParam(query.labelIds);
|
||||||
const labelsFeatureEnabled = await labelsEnabled(orgId);
|
const searchPublic = buildSearchConditionForPublic(query.query);
|
||||||
const searchPublic = buildSearchConditionForPublic(
|
const searchSite = buildSearchConditionForSiteResource(query.query);
|
||||||
query.query,
|
|
||||||
labelsFeatureEnabled
|
|
||||||
);
|
|
||||||
const searchSite = buildSearchConditionForSiteResource(
|
|
||||||
query.query,
|
|
||||||
labelsFeatureEnabled
|
|
||||||
);
|
|
||||||
const siteCountMap = new Map<number, SiteGroupRow>();
|
const siteCountMap = new Map<number, SiteGroupRow>();
|
||||||
|
|
||||||
if (accessible.resourceIds.length > 0) {
|
if (accessible.resourceIds.length > 0) {
|
||||||
@@ -822,10 +786,6 @@ async function listLabelGroups(
|
|||||||
>();
|
>();
|
||||||
let unlabeledCount = 0;
|
let unlabeledCount = 0;
|
||||||
|
|
||||||
if (!(await labelsEnabled(orgId))) {
|
|
||||||
return { groups: [], total: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
const matchesLabelFilters = (labelId: number) =>
|
const matchesLabelFilters = (labelId: number) =>
|
||||||
labelFilterIds.length === 0 || labelFilterIds.includes(labelId);
|
labelFilterIds.length === 0 || labelFilterIds.includes(labelId);
|
||||||
|
|
||||||
@@ -835,7 +795,7 @@ async function listLabelGroups(
|
|||||||
eq(resources.orgId, orgId),
|
eq(resources.orgId, orgId),
|
||||||
eq(resources.enabled, true)
|
eq(resources.enabled, true)
|
||||||
];
|
];
|
||||||
const searchPublic = buildSearchConditionForPublic(query.query, true);
|
const searchPublic = buildSearchConditionForPublic(query.query);
|
||||||
if (searchPublic) {
|
if (searchPublic) {
|
||||||
publicConditions.push(searchPublic);
|
publicConditions.push(searchPublic);
|
||||||
}
|
}
|
||||||
@@ -899,10 +859,7 @@ async function listLabelGroups(
|
|||||||
eq(siteResources.orgId, orgId),
|
eq(siteResources.orgId, orgId),
|
||||||
eq(siteResources.enabled, true)
|
eq(siteResources.enabled, true)
|
||||||
];
|
];
|
||||||
const searchSite = buildSearchConditionForSiteResource(
|
const searchSite = buildSearchConditionForSiteResource(query.query);
|
||||||
query.query,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
if (searchSite) {
|
if (searchSite) {
|
||||||
siteConditions.push(searchSite);
|
siteConditions.push(searchSite);
|
||||||
}
|
}
|
||||||
@@ -1349,23 +1306,19 @@ async function listLauncherResourcesForUserUncached(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelsFeatureEnabled = await labelsEnabled(orgId);
|
|
||||||
|
|
||||||
if (query.query.trim()) {
|
if (query.query.trim()) {
|
||||||
if (filteredResourceIds.length > 0) {
|
if (filteredResourceIds.length > 0) {
|
||||||
filteredResourceIds = await filterPublicResourceIdsByTextSearch(
|
filteredResourceIds = await filterPublicResourceIdsByTextSearch(
|
||||||
orgId,
|
orgId,
|
||||||
filteredResourceIds,
|
filteredResourceIds,
|
||||||
query.query,
|
query.query
|
||||||
labelsFeatureEnabled
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (filteredSiteResourceIds.length > 0) {
|
if (filteredSiteResourceIds.length > 0) {
|
||||||
filteredSiteResourceIds = await filterSiteResourceIdsByTextSearch(
|
filteredSiteResourceIds = await filterSiteResourceIdsByTextSearch(
|
||||||
orgId,
|
orgId,
|
||||||
filteredSiteResourceIds,
|
filteredSiteResourceIds,
|
||||||
query.query,
|
query.query
|
||||||
labelsFeatureEnabled
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1565,10 +1518,6 @@ async function collectAccessibleLabels(
|
|||||||
): Promise<Map<number, LauncherLabel>> {
|
): Promise<Map<number, LauncherLabel>> {
|
||||||
const labelMap = new Map<number, LauncherLabel>();
|
const labelMap = new Map<number, LauncherLabel>();
|
||||||
|
|
||||||
if (!(await labelsEnabled(orgId))) {
|
|
||||||
return labelMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessible.resourceIds.length > 0) {
|
if (accessible.resourceIds.length > 0) {
|
||||||
const publicConditions = [
|
const publicConditions = [
|
||||||
inArray(resources.resourceId, accessible.resourceIds),
|
inArray(resources.resourceId, accessible.resourceIds),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const handleApplyBlueprintMessage: MessageHandler = async (context) => {
|
|||||||
source: "NEWT"
|
source: "NEWT"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to update database from config: ${error}`);
|
logger.debug(`Failed to update database from config: ${error}`);
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
type: "newt/blueprint/results",
|
type: "newt/blueprint/results",
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { resources, resourceWhitelist } from "@server/db";
|
import {
|
||||||
|
resources,
|
||||||
|
resourceWhitelist,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyWhiteList
|
||||||
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -103,40 +108,99 @@ export async function addEmailToResourceWhitelist(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resource.emailWhitelistEnabled) {
|
// A shared policy takes precedence over the resource's inline
|
||||||
return next(
|
// (default) policy, which takes precedence over the resource's own
|
||||||
createHttpError(
|
// direct whitelist fields. This mirrors the precedence used at
|
||||||
HttpCode.BAD_REQUEST,
|
// request time in authWithWhitelist.ts / getResourceAuthInfo.ts.
|
||||||
"Email whitelist is not enabled for this resource"
|
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, {
|
return response(res, {
|
||||||
data: {},
|
data: {},
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -96,13 +96,17 @@ export async function getResourceWhitelist(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInlinePolicy =
|
// A shared policy takes precedence over the resource's inline
|
||||||
resource.resourcePolicyId === null &&
|
// (default) policy, which takes precedence over the resource's own
|
||||||
resource.defaultResourcePolicyId !== null;
|
// 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
|
const whitelist =
|
||||||
? await queryPolicyWhitelist(resource.defaultResourcePolicyId!)
|
policyId !== null
|
||||||
: await queryWhitelist(resourceId);
|
? await queryPolicyWhitelist(policyId)
|
||||||
|
: await queryWhitelist(resourceId);
|
||||||
|
|
||||||
return response<GetResourceWhitelistResponse>(res, {
|
return response<GetResourceWhitelistResponse>(res, {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -363,11 +363,6 @@ export async function getUserResources(
|
|||||||
(r) => r.siteResourceId
|
(r) => r.siteResourceId
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.labels
|
|
||||||
);
|
|
||||||
|
|
||||||
let labelsForResources: Array<{
|
let labelsForResources: Array<{
|
||||||
labelId: number;
|
labelId: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -381,49 +376,45 @@ export async function getUserResources(
|
|||||||
siteResourceId: number;
|
siteResourceId: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
[labelsForResources, labelsForSiteResources] = await Promise.all([
|
||||||
[labelsForResources, labelsForSiteResources] = await Promise.all([
|
resourceIdList.length === 0
|
||||||
resourceIdList.length === 0
|
? Promise.resolve([])
|
||||||
? Promise.resolve([])
|
: db
|
||||||
: db
|
.select({
|
||||||
.select({
|
labelId: labels.labelId,
|
||||||
labelId: labels.labelId,
|
name: labels.name,
|
||||||
name: labels.name,
|
color: labels.color,
|
||||||
color: labels.color,
|
resourceId: resourceLabels.resourceId
|
||||||
resourceId: resourceLabels.resourceId
|
})
|
||||||
})
|
.from(labels)
|
||||||
.from(labels)
|
.innerJoin(
|
||||||
.innerJoin(
|
resourceLabels,
|
||||||
resourceLabels,
|
eq(resourceLabels.labelId, labels.labelId)
|
||||||
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(siteResourceLabels.siteResourceLabelId))
|
||||||
)
|
]);
|
||||||
.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))
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for password, pincode, and whitelist protection for each resource
|
// Check for password, pincode, and whitelist protection for each resource
|
||||||
const resourcesWithAuth = await Promise.all(
|
const resourcesWithAuth = await Promise.all(
|
||||||
|
|||||||
@@ -484,11 +484,6 @@ export async function listResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.labels
|
|
||||||
);
|
|
||||||
|
|
||||||
let accessibleResources: Array<{ resourceId: number }>;
|
let accessibleResources: Array<{ resourceId: number }>;
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
accessibleResources = await db
|
accessibleResources = await db
|
||||||
@@ -676,7 +671,7 @@ export async function listResources(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
if (labelFilter && labelFilter.length > 0) {
|
||||||
conditions.push(
|
conditions.push(
|
||||||
inArray(
|
inArray(
|
||||||
resources.resourceId,
|
resources.resourceId,
|
||||||
@@ -697,25 +692,20 @@ export async function listResources(
|
|||||||
const queryList = [
|
const queryList = [
|
||||||
like(sql`LOWER(${resources.name})`, q),
|
like(sql`LOWER(${resources.name})`, q),
|
||||||
like(sql`LOWER(${resources.niceId})`, 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));
|
conditions.push(or(...queryList));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,27 +737,23 @@ export async function listResources(
|
|||||||
resourceId: number;
|
resourceId: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
labelsForResources =
|
||||||
labelsForResources =
|
resourceIdList.length === 0
|
||||||
resourceIdList.length === 0
|
? []
|
||||||
? []
|
: await db
|
||||||
: await db
|
.select({
|
||||||
.select({
|
labelId: labels.labelId,
|
||||||
labelId: labels.labelId,
|
name: labels.name,
|
||||||
name: labels.name,
|
color: labels.color,
|
||||||
color: labels.color,
|
resourceId: resourceLabels.resourceId
|
||||||
resourceId: resourceLabels.resourceId
|
})
|
||||||
})
|
.from(labels)
|
||||||
.from(labels)
|
.innerJoin(
|
||||||
.innerJoin(
|
resourceLabels,
|
||||||
resourceLabels,
|
eq(resourceLabels.labelId, labels.labelId)
|
||||||
eq(resourceLabels.labelId, labels.labelId)
|
)
|
||||||
)
|
.where(inArray(resourceLabels.resourceId, resourceIdList))
|
||||||
.where(
|
.orderBy(asc(resourceLabels.resourceLabelId));
|
||||||
inArray(resourceLabels.resourceId, resourceIdList)
|
|
||||||
)
|
|
||||||
.orderBy(asc(resourceLabels.resourceLabelId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const allResourceTargets =
|
const allResourceTargets =
|
||||||
resourceIdList.length === 0
|
resourceIdList.length === 0
|
||||||
|
|||||||
@@ -248,11 +248,6 @@ export async function listUserResourceAliases(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.labels
|
|
||||||
);
|
|
||||||
|
|
||||||
const whereConditions = [
|
const whereConditions = [
|
||||||
eq(siteResources.orgId, orgId),
|
eq(siteResources.orgId, orgId),
|
||||||
eq(siteResources.enabled, true),
|
eq(siteResources.enabled, true),
|
||||||
@@ -262,7 +257,7 @@ export async function listUserResourceAliases(
|
|||||||
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
|
inArray(siteResources.siteResourceId, accessibleSiteResourceIds)
|
||||||
];
|
];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
if (labelFilter && labelFilter.length > 0) {
|
||||||
whereConditions.push(
|
whereConditions.push(
|
||||||
inArray(
|
inArray(
|
||||||
siteResources.siteResourceId,
|
siteResources.siteResourceId,
|
||||||
@@ -310,7 +305,7 @@ export async function listUserResourceAliases(
|
|||||||
siteResourceId: number;
|
siteResourceId: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && siteResourceIdList.length > 0) {
|
if (siteResourceIdList.length > 0) {
|
||||||
labelsForSiteResources = await db
|
labelsForSiteResources = await db
|
||||||
.select({
|
.select({
|
||||||
name: labels.name,
|
name: labels.name,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { resources, resourceWhitelist } from "@server/db";
|
import {
|
||||||
|
resources,
|
||||||
|
resourceWhitelist,
|
||||||
|
resourcePolicies,
|
||||||
|
resourcePolicyWhiteList
|
||||||
|
} from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -102,44 +107,110 @@ export async function removeEmailFromResourceWhitelist(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!resource.emailWhitelistEnabled) {
|
// A shared policy takes precedence over the resource's inline
|
||||||
return next(
|
// (default) policy, which takes precedence over the resource's own
|
||||||
createHttpError(
|
// direct whitelist fields. This mirrors the precedence used at
|
||||||
HttpCode.BAD_REQUEST,
|
// request time in authWithWhitelist.ts / getResourceAuthInfo.ts.
|
||||||
"Email whitelist is not enabled for this resource"
|
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, {
|
return response(res, {
|
||||||
data: {},
|
data: {},
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -109,13 +109,14 @@ export async function setResourceWhitelist(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInlinePolicy =
|
// A shared policy takes precedence over the resource's inline
|
||||||
resource.resourcePolicyId === null &&
|
// (default) policy, which takes precedence over the resource's own
|
||||||
resource.defaultResourcePolicyId !== null;
|
// direct whitelist fields. This mirrors the precedence used at
|
||||||
|
// request time in authWithWhitelist.ts / getResourceAuthInfo.ts.
|
||||||
if (isInlinePolicy) {
|
const policyId =
|
||||||
const policyId = resource.defaultResourcePolicyId!;
|
resource.resourcePolicyId ?? resource.defaultResourcePolicyId;
|
||||||
|
|
||||||
|
if (policyId !== null) {
|
||||||
const [policy] = await db
|
const [policy] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourcePolicies)
|
.from(resourcePolicies)
|
||||||
|
|||||||
@@ -235,11 +235,6 @@ export async function listSites(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.labels
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pageSize,
|
pageSize,
|
||||||
page,
|
page,
|
||||||
@@ -291,7 +286,7 @@ export async function listSites(
|
|||||||
conditions.push(eq(sites.status, status));
|
conditions.push(eq(sites.status, status));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
if (labelFilter && labelFilter.length > 0) {
|
||||||
conditions.push(
|
conditions.push(
|
||||||
inArray(
|
inArray(
|
||||||
sites.siteId,
|
sites.siteId,
|
||||||
@@ -311,24 +306,20 @@ export async function listSites(
|
|||||||
const q = "%" + query.toLowerCase() + "%";
|
const q = "%" + query.toLowerCase() + "%";
|
||||||
const queryList = [
|
const queryList = [
|
||||||
like(sql`LOWER(${sites.name})`, q),
|
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)!);
|
conditions.push(or(...queryList)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,25 +357,23 @@ export async function listSites(
|
|||||||
siteId: number;
|
siteId: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
labelsForSites =
|
||||||
labelsForSites =
|
siteIds.length === 0
|
||||||
siteIds.length === 0
|
? []
|
||||||
? []
|
: await db
|
||||||
: await db
|
.select({
|
||||||
.select({
|
labelId: labels.labelId,
|
||||||
labelId: labels.labelId,
|
name: labels.name,
|
||||||
name: labels.name,
|
color: labels.color,
|
||||||
color: labels.color,
|
siteId: siteLabels.siteId
|
||||||
siteId: siteLabels.siteId
|
})
|
||||||
})
|
.from(labels)
|
||||||
.from(labels)
|
.innerJoin(
|
||||||
.innerJoin(
|
siteLabels,
|
||||||
siteLabels,
|
eq(siteLabels.labelId, labels.labelId)
|
||||||
eq(siteLabels.labelId, labels.labelId)
|
)
|
||||||
)
|
.where(inArray(siteLabels.siteId, siteIds))
|
||||||
.where(inArray(siteLabels.siteId, siteIds))
|
.orderBy(asc(siteLabels.siteLabelId));
|
||||||
.orderBy(asc(siteLabels.siteLabelId));
|
|
||||||
}
|
|
||||||
|
|
||||||
const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => {
|
const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => {
|
||||||
const siteWithUpdate: SiteWithUpdateAvailable = { ...site };
|
const siteWithUpdate: SiteWithUpdateAvailable = { ...site };
|
||||||
|
|||||||
@@ -286,11 +286,6 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
labels: labelFilter
|
labels: labelFilter
|
||||||
} = parsedQuery.data;
|
} = parsedQuery.data;
|
||||||
|
|
||||||
const isLabelFeatureEnabled = await isLicensedOrSubscribed(
|
|
||||||
orgId,
|
|
||||||
tierMatrix.labels
|
|
||||||
);
|
|
||||||
|
|
||||||
const conditions = [and(eq(siteResources.orgId, orgId))];
|
const conditions = [and(eq(siteResources.orgId, orgId))];
|
||||||
|
|
||||||
if (siteId != null) {
|
if (siteId != null) {
|
||||||
@@ -320,7 +315,7 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
conditions.push(eq(siteResources.mode, mode));
|
conditions.push(eq(siteResources.mode, mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && labelFilter && labelFilter.length > 0) {
|
if (labelFilter && labelFilter.length > 0) {
|
||||||
conditions.push(
|
conditions.push(
|
||||||
inArray(
|
inArray(
|
||||||
siteResources.siteResourceId,
|
siteResources.siteResourceId,
|
||||||
@@ -344,25 +339,20 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
like(sql`LOWER(${siteResources.destination})`, q),
|
like(sql`LOWER(${siteResources.destination})`, q),
|
||||||
like(sql`LOWER(${siteResources.alias})`, q),
|
like(sql`LOWER(${siteResources.alias})`, q),
|
||||||
like(sql`LOWER(${siteResources.aliasAddress})`, 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));
|
conditions.push(or(...queryList));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +393,7 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
siteResourceId: number;
|
siteResourceId: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (isLabelFeatureEnabled && siteResourceIdList.length > 0) {
|
if (siteResourceIdList.length > 0) {
|
||||||
labelsForSiteResources = await db
|
labelsForSiteResources = await db
|
||||||
.select({
|
.select({
|
||||||
labelId: labels.labelId,
|
labelId: labels.labelId,
|
||||||
|
|||||||
@@ -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",
|
"Fully qualified domain name with optional wildcards, e.g., example.internal, *.example.internal, or host-0?.example.internal",
|
||||||
example: "service.example.internal"
|
example: "service.example.internal"
|
||||||
}),
|
}),
|
||||||
userIds: z.array(z.string()),
|
userIds: z.array(z.string()).optional(),
|
||||||
roleIds: z.array(z.int()),
|
roleIds: z.array(z.int()).optional(),
|
||||||
clientIds: z.array(z.int()),
|
clientIds: z.array(z.int()).optional(),
|
||||||
tcpPortRangeString: portRangeStringSchema,
|
tcpPortRangeString: portRangeStringSchema,
|
||||||
udpPortRangeString: portRangeStringSchema,
|
udpPortRangeString: portRangeStringSchema,
|
||||||
disableIcmp: z.boolean().optional(),
|
disableIcmp: z.boolean().optional(),
|
||||||
@@ -167,6 +167,10 @@ const updateSiteResourceSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
// if neither is provided, the existing site associations are left unchanged
|
||||||
|
if (data.siteIds === undefined && data.siteId === undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
(data.siteIds !== undefined && data.siteIds.length > 0) ||
|
(data.siteIds !== undefined && data.siteIds.length > 0) ||
|
||||||
data.siteId !== undefined
|
data.siteId !== undefined
|
||||||
@@ -263,7 +267,7 @@ export async function updateSiteResource(
|
|||||||
const { siteResourceId } = parsedParams.data;
|
const { siteResourceId } = parsedParams.data;
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
siteIds: siteIdsInput = [], // because it can change
|
siteIds: siteIdsInput,
|
||||||
siteId,
|
siteId,
|
||||||
niceId,
|
niceId,
|
||||||
mode,
|
mode,
|
||||||
@@ -287,9 +291,14 @@ export async function updateSiteResource(
|
|||||||
} = parsedBody.data;
|
} = parsedBody.data;
|
||||||
|
|
||||||
// Backward compatibility: merge deprecated siteId into siteIds array
|
// Backward compatibility: merge deprecated siteId into siteIds array
|
||||||
const siteIds = [...siteIdsInput];
|
const siteIdsProvided =
|
||||||
if (siteId !== undefined && !siteIds.includes(siteId)) {
|
siteIdsInput !== undefined || siteId !== undefined;
|
||||||
siteIds.push(siteId);
|
let siteIds: number[] | undefined;
|
||||||
|
if (siteIdsProvided) {
|
||||||
|
siteIds = [...(siteIdsInput ?? [])];
|
||||||
|
if (siteId !== undefined && !siteIds.includes(siteId)) {
|
||||||
|
siteIds.push(siteId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if site resource exists
|
// Check if site resource exists
|
||||||
@@ -356,20 +365,22 @@ export async function updateSiteResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify the site exists and belongs to the org
|
// Verify the site exists and belongs to the org
|
||||||
const sitesToAssign = await db
|
if (siteIds !== undefined) {
|
||||||
.select()
|
const sitesToAssign = await db
|
||||||
.from(sites)
|
.select()
|
||||||
.where(
|
.from(sites)
|
||||||
and(
|
.where(
|
||||||
inArray(sites.siteId, siteIds),
|
and(
|
||||||
eq(sites.orgId, existingSiteResource.orgId)
|
inArray(sites.siteId, siteIds),
|
||||||
)
|
eq(sites.orgId, existingSiteResource.orgId)
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
|
||||||
if (sitesToAssign.length !== siteIds.length) {
|
if (sitesToAssign.length !== siteIds.length) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.NOT_FOUND, "Some site not found")
|
createHttpError(HttpCode.NOT_FOUND, "Some site not found")
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only check if destination is an IP address
|
// Only check if destination is an IP address
|
||||||
@@ -463,7 +474,8 @@ export async function updateSiteResource(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let updatedSiteResource: SiteResource | undefined;
|
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) => {
|
await db.transaction(async (trx) => {
|
||||||
// Update the site resource
|
// Update the site resource
|
||||||
const sshPamSet =
|
const sshPamSet =
|
||||||
@@ -522,81 +534,103 @@ export async function updateSiteResource(
|
|||||||
|
|
||||||
//////////////////// update the associations ////////////////////
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
// delete the site - site resources associations
|
if (siteIds !== undefined) {
|
||||||
await trx
|
// delete the site - site resources associations
|
||||||
.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 {
|
|
||||||
await trx
|
await trx
|
||||||
.delete(roleSiteResources)
|
.delete(siteNetworks)
|
||||||
.where(
|
.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) {
|
if (clientIds !== undefined) {
|
||||||
await trx.insert(roleSiteResources).values(
|
await trx
|
||||||
roleIds.map((roleId) => ({
|
.delete(clientSiteResources)
|
||||||
roleId,
|
.where(
|
||||||
siteResourceId
|
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}`);
|
logger.info(`Updated site resource ${siteResourceId}`);
|
||||||
|
|||||||
-4
@@ -3,9 +3,7 @@ import { authCookieHeader } from "@app/lib/api/cookies";
|
|||||||
import { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
import { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import OrgLabelsTable from "@app/components/OrgLabelsTable";
|
import OrgLabelsTable from "@app/components/OrgLabelsTable";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
@@ -51,8 +49,6 @@ export default async function LabelsPage({ params, searchParams }: Props) {
|
|||||||
description={t("orgLabelsDescription")}
|
description={t("orgLabelsDescription")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PaidFeaturesAlert tiers={tierMatrix.labels} />
|
|
||||||
|
|
||||||
<OrgLabelsTable
|
<OrgLabelsTable
|
||||||
labels={labels}
|
labels={labels}
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
@@ -103,7 +103,6 @@ export default function MachineClientsTable({
|
|||||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
|
||||||
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
const data = useQuery(productUpdatesQueries.latestVersion(true));
|
||||||
|
|
||||||
const latestPlatformVersions = data.data?.data;
|
const latestPlatformVersions = data.data?.data;
|
||||||
@@ -434,11 +433,8 @@ export default function MachineClientsTable({
|
|||||||
accessorKey: "subnet",
|
accessorKey: "subnet",
|
||||||
friendlyName: t("address"),
|
friendlyName: t("address"),
|
||||||
header: () => <span className="px-3">{t("address")}</span>
|
header: () => <span className="px-3">{t("address")}</span>
|
||||||
}
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
|
||||||
baseColumns.push({
|
|
||||||
id: "labels",
|
id: "labels",
|
||||||
accessorKey: "labels",
|
accessorKey: "labels",
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -458,8 +454,8 @@ export default function MachineClientsTable({
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
// Only include actions column if there are rows without userIds
|
// Only include actions column if there are rows without userIds
|
||||||
if (hasRowsWithoutUserId) {
|
if (hasRowsWithoutUserId) {
|
||||||
@@ -541,7 +537,7 @@ export default function MachineClientsTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return baseColumns;
|
return baseColumns;
|
||||||
}, [hasRowsWithoutUserId, isLabelFeatureEnabled, orgId, t, searchParams]);
|
}, [hasRowsWithoutUserId, orgId, t, searchParams]);
|
||||||
|
|
||||||
function handleFilterChange(
|
function handleFilterChange(
|
||||||
column: string,
|
column: string,
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ import {
|
|||||||
ControlledDataTable,
|
ControlledDataTable,
|
||||||
type ExtendedColumnDef
|
type ExtendedColumnDef
|
||||||
} from "./ui/controlled-data-table";
|
} 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 ConfirmDeleteDialog from "./ConfirmDeleteDialog";
|
||||||
import { CreateOrgLabelDialog } from "./CreateOrgLabelDialog";
|
import { CreateOrgLabelDialog } from "./CreateOrgLabelDialog";
|
||||||
import { EditOrgLabelDialog } from "./EditOrgLabelDialog";
|
import { EditOrgLabelDialog } from "./EditOrgLabelDialog";
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ export default function PrivateResourcesTable({
|
|||||||
const [isRefreshing, startRefreshTransition] = useTransition();
|
const [isRefreshing, startRefreshTransition] = useTransition();
|
||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const interval = setInterval(() => {
|
// const interval = setInterval(() => {
|
||||||
@@ -488,11 +487,8 @@ export default function PrivateResourcesTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
|
||||||
cols.splice(cols.length - 1, 0, {
|
|
||||||
id: "labels",
|
id: "labels",
|
||||||
accessorKey: "labels",
|
accessorKey: "labels",
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -512,11 +508,11 @@ export default function PrivateResourcesTable({
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [isLabelFeatureEnabled, orgId, t, searchParams]);
|
}, [orgId, t, searchParams]);
|
||||||
|
|
||||||
function handleFilterChange(
|
function handleFilterChange(
|
||||||
column: string,
|
column: string,
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ export default function PublicResourcesTable({
|
|||||||
useState<ResourceRow | null>();
|
useState<ResourceRow | null>();
|
||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
|
||||||
|
|
||||||
const [isRefreshing, startTransition] = useTransition();
|
const [isRefreshing, startTransition] = useTransition();
|
||||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||||
@@ -604,11 +603,8 @@ export default function PublicResourcesTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
|
||||||
cols.splice(cols.length - 1, 0, {
|
|
||||||
id: "labels",
|
id: "labels",
|
||||||
accessorKey: "labels",
|
accessorKey: "labels",
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -625,11 +621,11 @@ export default function PublicResourcesTable({
|
|||||||
cell: ({ row }: { row: { original: ResourceRow } }) => (
|
cell: ({ row }: { row: { original: ResourceRow } }) => (
|
||||||
<ResourceLabelCell resource={row.original} orgId={orgId} />
|
<ResourceLabelCell resource={row.original} orgId={orgId} />
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [isLabelFeatureEnabled, orgId, t, searchParams]);
|
}, [orgId, t, searchParams]);
|
||||||
|
|
||||||
function handleFilterChange(
|
function handleFilterChange(
|
||||||
column: string,
|
column: string,
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ import {
|
|||||||
|
|
||||||
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
|
import { useOptimisticLabels } from "@app/hooks/useOptimisticLabels";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
|
||||||
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
import { LabelColumnFilterButton } from "./LabelColumnFilterButton";
|
||||||
import { LabelsTableCell } from "./LabelsTableCell";
|
import { LabelsTableCell } from "./LabelsTableCell";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -114,7 +113,6 @@ export default function SitesTable({
|
|||||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels);
|
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -171,7 +169,10 @@ export default function SitesTable({
|
|||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("siteErrorRestart"),
|
title: t("siteErrorRestart"),
|
||||||
description: formatAxiosError(e, t("siteErrorRestartDescription"))
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("siteErrorRestartDescription")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setRestartingSite(null);
|
setRestartingSite(null);
|
||||||
@@ -598,11 +599,8 @@ export default function SitesTable({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
];
|
{
|
||||||
|
|
||||||
if (isLabelFeatureEnabled) {
|
|
||||||
cols.splice(cols.length - 1, 0, {
|
|
||||||
accessorKey: "labels",
|
accessorKey: "labels",
|
||||||
header: () => (
|
header: () => (
|
||||||
<LabelColumnFilterButton
|
<LabelColumnFilterButton
|
||||||
@@ -618,11 +616,11 @@ export default function SitesTable({
|
|||||||
cell: ({ row }: { row: { original: SiteRow } }) => (
|
cell: ({ row }: { row: { original: SiteRow } }) => (
|
||||||
<SiteLabelCell site={row.original} orgId={orgId} />
|
<SiteLabelCell site={row.original} orgId={orgId} />
|
||||||
)
|
)
|
||||||
});
|
}
|
||||||
}
|
];
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [isLabelFeatureEnabled, orgId, t, searchParams, latestNewtVersion]);
|
}, [orgId, t, searchParams, latestNewtVersion]);
|
||||||
|
|
||||||
function toggleSort(column: string) {
|
function toggleSort(column: string) {
|
||||||
const newSearch = getNextSortOrder(column, searchParams);
|
const newSearch = getNextSortOrder(column, searchParams);
|
||||||
|
|||||||
Reference in New Issue
Block a user