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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
clients,
|
||||
clientLabels,
|
||||
db,
|
||||
labels,
|
||||
resourceLabels,
|
||||
resources,
|
||||
siteLabels,
|
||||
siteResourceLabels,
|
||||
siteResources,
|
||||
sites
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty(),
|
||||
labelId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const attachLabelBodySchema = z.strictObject({
|
||||
siteId: z.number().int().optional(),
|
||||
resourceId: z.number().int().optional(),
|
||||
siteResourceId: z.number().int().optional(),
|
||||
clientId: z.number().int().optional()
|
||||
});
|
||||
|
||||
export async function attachLabelToItem(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, labelId } = parsedParams.data;
|
||||
|
||||
const parsedBody = attachLabelBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId, resourceId, siteResourceId, clientId } =
|
||||
parsedBody.data;
|
||||
|
||||
if (!siteId && !resourceId && !siteResourceId && !clientId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"At least one of `siteId`, `resourceId`, `siteResourceId` or `clientId` should be provided."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(labels)
|
||||
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Label with Id ${labelId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (siteId) {
|
||||
const siteCount = await db.$count(
|
||||
sites,
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
);
|
||||
|
||||
if (siteCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with Id ${siteId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// idempotent, calling this endpoint multiple times should attach the label only once
|
||||
await db
|
||||
.insert(siteLabels)
|
||||
.values({
|
||||
labelId,
|
||||
siteId
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
if (resourceId) {
|
||||
const resourceCount = await db.$count(
|
||||
resources,
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
eq(resources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with Id ${resourceId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// idempotent, calling this endpoint multiple times should attach the label only once
|
||||
await db
|
||||
.insert(resourceLabels)
|
||||
.values({
|
||||
labelId,
|
||||
resourceId
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
if (siteResourceId) {
|
||||
const resourceCount = await db.$count(
|
||||
siteResources,
|
||||
and(
|
||||
eq(siteResources.siteResourceId, siteResourceId),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`SiteResource with Id ${siteResourceId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// idempotent, calling this endpoint multiple times should attach the label only once
|
||||
await db
|
||||
.insert(siteResourceLabels)
|
||||
.values({
|
||||
labelId,
|
||||
siteResourceId
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
if (clientId) {
|
||||
const clientCount = await db.$count(
|
||||
clients,
|
||||
and(
|
||||
eq(clients.clientId, clientId),
|
||||
eq(clients.orgId, orgId),
|
||||
isNull(clients.userId)
|
||||
)
|
||||
);
|
||||
|
||||
if (clientCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Client with Id ${clientId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// idempotent, calling this endpoint multiple times should attach the label only once
|
||||
await db
|
||||
.insert(clientLabels)
|
||||
.values({
|
||||
labelId,
|
||||
clientId
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Label attached successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
db,
|
||||
labels,
|
||||
resourceLabels,
|
||||
resources,
|
||||
siteLabels,
|
||||
sites
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const bodySchema = z.strictObject({
|
||||
name: z.string().nonempty(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i)
|
||||
.nonempty(),
|
||||
siteId: z.number().int().optional(),
|
||||
resourceId: z.number().int().optional()
|
||||
});
|
||||
|
||||
export async function createOrgLabel(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const parsedBody = bodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { name, color, siteId, resourceId } = parsedBody.data;
|
||||
|
||||
if (siteId) {
|
||||
const siteCount = await db.$count(
|
||||
sites,
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
);
|
||||
|
||||
if (siteCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Site with Id ${siteId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceId) {
|
||||
const resourceCount = await db.$count(
|
||||
resources,
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
eq(resources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
`Resource with Id ${resourceId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [existingLabel] = await db
|
||||
.select({ labelId: labels.labelId })
|
||||
.from(labels)
|
||||
.where(
|
||||
and(
|
||||
eq(labels.orgId, orgId),
|
||||
sql`LOWER(${labels.name}) = ${name.toLowerCase()}`
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingLabel) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"A label with this name already exists"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const label = await db.transaction(async (tx) => {
|
||||
const [label] = await tx
|
||||
.insert(labels)
|
||||
.values({
|
||||
name,
|
||||
color,
|
||||
orgId
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (siteId) {
|
||||
await tx.insert(siteLabels).values({
|
||||
siteId,
|
||||
labelId: label.labelId
|
||||
});
|
||||
}
|
||||
|
||||
if (resourceId) {
|
||||
await tx.insert(resourceLabels).values({
|
||||
resourceId,
|
||||
labelId: label.labelId
|
||||
});
|
||||
}
|
||||
return label;
|
||||
});
|
||||
|
||||
return response<CreateOrEditLabelResponse>(res, {
|
||||
data: { label },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Org Label created successfully",
|
||||
status: HttpCode.CREATED
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { db, labels } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty(),
|
||||
labelId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
export async function deleteOrgLabel(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, labelId } = parsedParams.data;
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(labels)
|
||||
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
|
||||
|
||||
if (!existing) {
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Label not found"));
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(labels)
|
||||
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
|
||||
|
||||
return response(res, {
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Label deleted successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
clients,
|
||||
clientLabels,
|
||||
db,
|
||||
labels,
|
||||
resourceLabels,
|
||||
resources,
|
||||
siteLabels,
|
||||
siteResourceLabels,
|
||||
siteResources,
|
||||
sites
|
||||
} from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty(),
|
||||
labelId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const detachLabelBodySchema = z.strictObject({
|
||||
siteId: z.number().int().optional(),
|
||||
resourceId: z.number().int().optional(),
|
||||
siteResourceId: z.number().int().optional(),
|
||||
clientId: z.number().int().optional()
|
||||
});
|
||||
|
||||
export async function detachLabelFromItem(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, labelId } = parsedParams.data;
|
||||
|
||||
const parsedBody = detachLabelBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { siteId, resourceId, siteResourceId, clientId } =
|
||||
parsedBody.data;
|
||||
|
||||
if (!siteId && !resourceId && !siteResourceId && !clientId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"At least one of `siteId`, `resourceId`, `siteResourceId` or `clientId` should be provided."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(labels)
|
||||
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
|
||||
|
||||
if (!existing) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Label with Id ${labelId} not found`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (siteId) {
|
||||
const siteCount = await db.$count(
|
||||
sites,
|
||||
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
|
||||
);
|
||||
|
||||
if (siteCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Site with Id ${siteId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(siteLabels)
|
||||
.where(
|
||||
and(
|
||||
eq(siteLabels.labelId, labelId),
|
||||
eq(siteLabels.siteId, siteId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceId) {
|
||||
const resourceCount = await db.$count(
|
||||
resources,
|
||||
and(
|
||||
eq(resources.resourceId, resourceId),
|
||||
eq(resources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Resource with Id ${resourceId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(resourceLabels)
|
||||
.where(
|
||||
and(
|
||||
eq(resourceLabels.labelId, labelId),
|
||||
eq(resourceLabels.resourceId, resourceId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (siteResourceId) {
|
||||
const resourceCount = await db.$count(
|
||||
siteResources,
|
||||
and(
|
||||
eq(siteResources.siteResourceId, siteResourceId),
|
||||
eq(siteResources.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
if (resourceCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`SiteResource with Id ${siteResourceId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(siteResourceLabels)
|
||||
.where(
|
||||
and(
|
||||
eq(siteResourceLabels.labelId, labelId),
|
||||
eq(siteResourceLabels.siteResourceId, siteResourceId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (clientId) {
|
||||
const clientCount = await db.$count(
|
||||
clients,
|
||||
and(
|
||||
eq(clients.clientId, clientId),
|
||||
eq(clients.orgId, orgId),
|
||||
isNull(clients.userId)
|
||||
)
|
||||
);
|
||||
|
||||
if (clientCount === 0) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
`Client with Id ${clientId} doesn't exist.`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(clientLabels)
|
||||
.where(
|
||||
and(
|
||||
eq(clientLabels.labelId, labelId),
|
||||
eq(clientLabels.clientId, clientId)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return response(res, {
|
||||
data: {},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Label detached successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./listOrgLabels";
|
||||
export * from "./createOrgLabel";
|
||||
export * from "./updateOrgLabel";
|
||||
export * from "./attachLabelToItem";
|
||||
export * from "./detachLabelFromItem";
|
||||
export * from "./deleteOrgLabel";
|
||||
@@ -0,0 +1,142 @@
|
||||
import { db, labels } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, asc, eq, like, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty()
|
||||
});
|
||||
|
||||
const listLabelsSchema = z.object({
|
||||
pageSize: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
.positive()
|
||||
.optional()
|
||||
.catch(20)
|
||||
.default(20)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 20,
|
||||
description: "Number of items per page"
|
||||
}),
|
||||
page: z.coerce
|
||||
.number<string>() // for prettier formatting
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.catch(1)
|
||||
.default(1)
|
||||
.openapi({
|
||||
type: "integer",
|
||||
default: 1,
|
||||
description: "Page number to retrieve"
|
||||
}),
|
||||
query: z.string().optional()
|
||||
});
|
||||
|
||||
function queryLabelsBase() {
|
||||
return db
|
||||
.select({
|
||||
labelId: labels.labelId,
|
||||
name: labels.name,
|
||||
color: labels.color
|
||||
})
|
||||
.from(labels);
|
||||
}
|
||||
|
||||
export async function listOrgLabels(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedQuery = listLabelsSchema.safeParse(req.query);
|
||||
if (!parsedQuery.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedQuery.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error)
|
||||
)
|
||||
);
|
||||
}
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
if (req.user && orgId && orgId !== req.userOrgId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.FORBIDDEN,
|
||||
"User does not have access to this organization"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { pageSize, page, query } = parsedQuery.data;
|
||||
|
||||
const conditions = [and(eq(labels.orgId, orgId))];
|
||||
|
||||
if (query) {
|
||||
conditions.push(
|
||||
like(
|
||||
sql`LOWER(${labels.name})`,
|
||||
"%" + query.toLowerCase() + "%"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const baseQuery = queryLabelsBase().where(and(...conditions));
|
||||
|
||||
// we need to add `as` so that drizzle filters the result as a subquery
|
||||
const countQuery = db.$count(
|
||||
queryLabelsBase()
|
||||
.where(and(...conditions))
|
||||
.as("filtered_labels")
|
||||
);
|
||||
|
||||
const labelListQuery = baseQuery
|
||||
.limit(pageSize)
|
||||
.offset(pageSize * (page - 1))
|
||||
.orderBy(asc(labels.name));
|
||||
|
||||
const [totalCount, rows] = await Promise.all([
|
||||
countQuery,
|
||||
labelListQuery
|
||||
]);
|
||||
|
||||
return response<ListOrgLabelsResponse>(res, {
|
||||
data: {
|
||||
labels: rows,
|
||||
pagination: {
|
||||
total: totalCount,
|
||||
pageSize,
|
||||
page
|
||||
}
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Labels retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { db, labels } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import type { CreateOrEditLabelResponse } from "@server/routers/labels/types";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { and, eq, ne, sql } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const paramsSchema = z.strictObject({
|
||||
orgId: z.string().nonempty(),
|
||||
labelId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
const updateLabelBodySchema = z.strictObject({
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i)
|
||||
.nonempty()
|
||||
});
|
||||
|
||||
export async function updateOrgLabel(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
try {
|
||||
const parsedParams = paramsSchema.safeParse(req.params);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { orgId, labelId } = parsedParams.data;
|
||||
|
||||
const parsedBody = updateLabelBodySchema.safeParse(req.body);
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(labels)
|
||||
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)));
|
||||
|
||||
if (!existing) {
|
||||
return next(createHttpError(HttpCode.NOT_FOUND, "Label not found"));
|
||||
}
|
||||
|
||||
const { name, color } = parsedBody.data;
|
||||
|
||||
if (name && name.toLowerCase() !== existing.name.toLowerCase()) {
|
||||
const [duplicateLabel] = await db
|
||||
.select({ labelId: labels.labelId })
|
||||
.from(labels)
|
||||
.where(
|
||||
and(
|
||||
eq(labels.orgId, orgId),
|
||||
ne(labels.labelId, labelId),
|
||||
sql`LOWER(${labels.name}) = ${name.toLowerCase()}`
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (duplicateLabel) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.CONFLICT,
|
||||
"A label with this name already exists"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const [label] = await db
|
||||
.update(labels)
|
||||
.set({
|
||||
name,
|
||||
color
|
||||
})
|
||||
.where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId)))
|
||||
.returning();
|
||||
|
||||
return response<CreateOrEditLabelResponse>(res, {
|
||||
data: {
|
||||
label
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Label updated successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -321,10 +321,7 @@ function combineOrConditions(
|
||||
return or(...parts);
|
||||
}
|
||||
|
||||
function buildSearchConditionForPublic(
|
||||
query: string,
|
||||
labelsFeatureEnabled: boolean
|
||||
) {
|
||||
function buildSearchConditionForPublic(query: string) {
|
||||
if (!query.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -342,32 +339,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;
|
||||
}
|
||||
@@ -382,40 +368,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<number[]> {
|
||||
if (!query.trim() || resourceIds.length === 0) {
|
||||
return resourceIds;
|
||||
}
|
||||
|
||||
const textMatch = combineOrConditions(
|
||||
buildSearchConditionForPublic(query, labelsFeatureEnabled),
|
||||
buildSearchConditionForPublic(query),
|
||||
buildSiteNameSearchCondition(query)
|
||||
);
|
||||
if (!textMatch) {
|
||||
@@ -442,15 +422,14 @@ async function filterPublicResourceIdsByTextSearch(
|
||||
async function filterSiteResourceIdsByTextSearch(
|
||||
orgId: string,
|
||||
siteResourceIds: number[],
|
||||
query: string,
|
||||
labelsFeatureEnabled: boolean
|
||||
query: string
|
||||
): Promise<number[]> {
|
||||
if (!query.trim() || siteResourceIds.length === 0) {
|
||||
return siteResourceIds;
|
||||
}
|
||||
|
||||
const textMatch = combineOrConditions(
|
||||
buildSearchConditionForSiteResource(query, labelsFeatureEnabled),
|
||||
buildSearchConditionForSiteResource(query),
|
||||
buildSiteNameSearchCondition(query)
|
||||
);
|
||||
if (!textMatch) {
|
||||
@@ -477,10 +456,6 @@ async function filterSiteResourceIdsByTextSearch(
|
||||
return rows.map((row) => row.siteResourceId);
|
||||
}
|
||||
|
||||
async function labelsEnabled(orgId: string): Promise<boolean> {
|
||||
return isLicensedOrSubscribed(orgId, tierMatrix.labels);
|
||||
}
|
||||
|
||||
async function fetchLabelsForResources(
|
||||
orgId: string,
|
||||
resourceIds: number[],
|
||||
@@ -492,10 +467,6 @@ async function fetchLabelsForResources(
|
||||
const byResourceId = new Map<number, LauncherLabel[]>();
|
||||
const bySiteResourceId = new Map<number, LauncherLabel[]>();
|
||||
|
||||
if (!(await labelsEnabled(orgId))) {
|
||||
return { byResourceId, bySiteResourceId };
|
||||
}
|
||||
|
||||
const [resourceLabelRows, siteResourceLabelRows] = await Promise.all([
|
||||
resourceIds.length === 0
|
||||
? Promise.resolve([])
|
||||
@@ -571,15 +542,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<number, SiteGroupRow>();
|
||||
|
||||
if (accessible.resourceIds.length > 0) {
|
||||
@@ -822,10 +786,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);
|
||||
|
||||
@@ -835,7 +795,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);
|
||||
}
|
||||
@@ -899,10 +859,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);
|
||||
}
|
||||
@@ -1349,23 +1306,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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1565,10 +1518,6 @@ async function collectAccessibleLabels(
|
||||
): Promise<Map<number, LauncherLabel>> {
|
||||
const labelMap = new Map<number, LauncherLabel>();
|
||||
|
||||
if (!(await labelsEnabled(orgId))) {
|
||||
return labelMap;
|
||||
}
|
||||
|
||||
if (accessible.resourceIds.length > 0) {
|
||||
const publicConditions = [
|
||||
inArray(resources.resourceId, accessible.resourceIds),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<GetResourceWhitelistResponse>(res, {
|
||||
data: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user