diff --git a/server/auth/actions.ts b/server/auth/actions.ts index f6bfa2549..5e2f58287 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -151,6 +151,7 @@ export enum ActionsEnum { listOrgLabels = "listOrgLabels", createOrgLabel = "createOrgLabel", updateOrgLabel = "updateOrgLabel", + attachLabelToItem = "attachLabelToItem", getAlertRule = "getAlertRule", createHealthCheck = "createHealthCheck", updateHealthCheck = "updateHealthCheck", diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 1b8c1ef4c..7941cd1fe 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -757,6 +757,14 @@ authenticated.patch( labels.updateOrgLabel ); +authenticated.put( + "/org/:orgId/label/:labelId/attach", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.attachLabelToItem), + labels.attachLabelToItem +); + authenticated.get( "/org/:orgId/health-checks", verifyValidLicense, diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/private/routers/labels/attachLabelToItem.ts new file mode 100644 index 000000000..4655c3f3b --- /dev/null +++ b/server/private/routers/labels/attachLabelToItem.ts @@ -0,0 +1,155 @@ +/* + * 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, + resourceLabels, + resources, + siteLabels, + sites +} 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()) +}); + +const attachLabelBodySchema = z.strictObject({ + siteId: z.number().int().optional(), + resourceId: 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 } = parsedBody.data; + + if (!siteId && !resourceId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "At least one of `siteId` or `resourceId` 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 + .insert(siteLabels) + .values({ + labelId, + siteId + }) + .returning(); + } + + 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.` + ) + ); + } + + await db.insert(resourceLabels).values({ + labelId, + resourceId + }); + } + + return response(res, { + data: {}, + success: true, + error: false, + message: "Site Label object created successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/labels/createOrgLabel.ts b/server/private/routers/labels/createOrgLabel.ts index 1439018d2..074a96207 100644 --- a/server/private/routers/labels/createOrgLabel.ts +++ b/server/private/routers/labels/createOrgLabel.ts @@ -16,14 +16,13 @@ import { resourceLabels, resources, siteLabels, - sites, - type Label + 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 { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -74,7 +73,10 @@ export async function createOrgLabel( const { name, color, siteId, resourceId } = parsedBody.data; if (siteId) { - const siteCount = await db.$count(sites, eq(sites.siteId, siteId)); + const siteCount = await db.$count( + sites, + and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) + ); if (siteCount === 0) { return next( @@ -88,8 +90,11 @@ export async function createOrgLabel( if (resourceId) { const resourceCount = await db.$count( - sites, - eq(resources.resourceId, resourceId) + resources, + and( + eq(resources.resourceId, resourceId), + eq(resources.orgId, orgId) + ) ); if (resourceCount === 0) { diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts index 4c7e5f432..cc15ebffe 100644 --- a/server/private/routers/labels/index.ts +++ b/server/private/routers/labels/index.ts @@ -14,3 +14,4 @@ export * from "./listOrgLabels"; export * from "./createOrgLabel"; export * from "./updateOrgLabel"; +export * from "./attachLabelToItem"; diff --git a/server/private/routers/labels/updateOrgLabel.ts b/server/private/routers/labels/updateOrgLabel.ts index d6478678a..eb5f5177a 100644 --- a/server/private/routers/labels/updateOrgLabel.ts +++ b/server/private/routers/labels/updateOrgLabel.ts @@ -11,15 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { - db, - labels, - resourceLabels, - resources, - siteLabels, - sites, - type Label -} from "@server/db"; +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";