/* * 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 { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, targetHealthCheck, newts, sites } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { and, eq, isNull } from "drizzle-orm"; import { addStandaloneHealthCheck } from "@server/routers/newt/targets"; import { fireHealthCheckUnhealthyAlert, fireHealthCheckUnknownAlert, fireHealthCheckHealthyAlert } from "@server/lib/alerts"; const paramsSchema = z .object({ orgId: z.string().nonempty(), healthCheckId: z .string() .transform(Number) .pipe(z.number().int().positive()) }) .strict(); const bodySchema = z.strictObject({ name: z.string().nonempty().optional(), siteId: z.number().int().positive().optional(), hcEnabled: z.boolean().optional(), hcMode: z.string().optional(), hcHostname: z.string().optional(), hcPort: z.number().int().min(1).max(65535).optional(), hcPath: z.string().optional(), hcScheme: z.string().optional(), hcMethod: z.string().optional(), hcInterval: z.number().int().positive().optional(), hcUnhealthyInterval: z.number().int().positive().optional(), hcTimeout: z.number().int().positive().optional(), hcHeaders: z.string().optional().nullable(), hcFollowRedirects: z.boolean().optional(), hcStatus: z.number().int().optional().nullable(), hcTlsServerName: z.string().optional(), hcHealthyThreshold: z.number().int().positive().optional(), hcUnhealthyThreshold: z.number().int().positive().optional() }); export type UpdateHealthCheckResponse = { targetHealthCheckId: number; name: string | null; siteId: number | null; hcEnabled: boolean; hcHealth: string | null; hcMode: string | null; hcHostname: string | null; hcPort: number | null; hcPath: string | null; hcScheme: string | null; hcMethod: string | null; hcInterval: number | null; hcUnhealthyInterval: number | null; hcTimeout: number | null; hcHeaders: string | null; hcFollowRedirects: boolean | null; hcStatus: number | null; hcTlsServerName: string | null; hcHealthyThreshold: number | null; hcUnhealthyThreshold: number | null; }; registry.registerPath({ method: "post", path: "/org/{orgId}/health-check/{healthCheckId}", description: "Update a health check for a specific organization.", tags: [OpenAPITags.Org], request: { params: paramsSchema, body: { content: { "application/json": { schema: bodySchema } } } }, responses: {} }); export async function updateHealthCheck( req: Request, res: Response, next: NextFunction ): Promise { try { const parsedParams = paramsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedParams.error).toString() ) ); } const { orgId, healthCheckId } = parsedParams.data; const parsedBody = bodySchema.safeParse(req.body); if (!parsedBody.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedBody.error).toString() ) ); } const [existing] = await db .select() .from(targetHealthCheck) .where( and( eq(targetHealthCheck.targetHealthCheckId, healthCheckId), eq(targetHealthCheck.orgId, orgId), isNull(targetHealthCheck.targetId) ) ); if (!existing) { return next( createHttpError( HttpCode.NOT_FOUND, "Standalone health check not found" ) ); } const { name, siteId, hcEnabled, hcMode, hcHostname, hcPort, hcPath, hcScheme, hcMethod, hcInterval, hcUnhealthyInterval, hcTimeout, hcHeaders, hcFollowRedirects, hcStatus, hcTlsServerName, hcHealthyThreshold, hcUnhealthyThreshold } = parsedBody.data; const updateData: Record = {}; const [existingHealthCheck] = await db .select() .from(targetHealthCheck) .where( and( eq(targetHealthCheck.targetHealthCheckId, healthCheckId), eq(targetHealthCheck.orgId, orgId) ) ) .limit(1); if (name !== undefined) updateData.name = name; if (siteId !== undefined) updateData.siteId = siteId; if (hcEnabled !== undefined) updateData.hcEnabled = hcEnabled; if (hcMode !== undefined) updateData.hcMode = hcMode; if (hcHostname !== undefined) updateData.hcHostname = hcHostname; if (hcPort !== undefined) updateData.hcPort = hcPort; if (hcPath !== undefined) updateData.hcPath = hcPath; if (hcScheme !== undefined) updateData.hcScheme = hcScheme; if (hcMethod !== undefined) updateData.hcMethod = hcMethod; if (hcInterval !== undefined) updateData.hcInterval = hcInterval; if (hcUnhealthyInterval !== undefined) updateData.hcUnhealthyInterval = hcUnhealthyInterval; if (hcTimeout !== undefined) updateData.hcTimeout = hcTimeout; if (hcHeaders !== undefined) updateData.hcHeaders = hcHeaders; if (hcFollowRedirects !== undefined) updateData.hcFollowRedirects = hcFollowRedirects; if (hcStatus !== undefined) updateData.hcStatus = hcStatus; if (hcTlsServerName !== undefined) updateData.hcTlsServerName = hcTlsServerName; if (hcHealthyThreshold !== undefined) updateData.hcHealthyThreshold = hcHealthyThreshold; if (hcUnhealthyThreshold !== undefined) updateData.hcUnhealthyThreshold = hcUnhealthyThreshold; const hcEnabledTurnedOn = parsedBody.data.hcEnabled === true && existingHealthCheck.hcEnabled === false; let hcHealthValue: "unknown" | "healthy" | "unhealthy" | undefined; if ( parsedBody.data.hcEnabled === false || parsedBody.data.hcEnabled === null ) { hcHealthValue = "unknown"; } else if (hcEnabledTurnedOn) { hcHealthValue = "unhealthy"; } else { hcHealthValue = undefined; } if (hcHealthValue) { updateData.hcHealth = hcHealthValue; } const [updated] = await db .update(targetHealthCheck) .set(updateData) .where( and( eq(targetHealthCheck.targetHealthCheckId, healthCheckId), eq(targetHealthCheck.orgId, orgId), isNull(targetHealthCheck.targetId) ) ) .returning(); if ( updated.hcHealth === "unhealthy" && existingHealthCheck.hcHealth !== "unhealthy" ) { await fireHealthCheckUnhealthyAlert( updated.orgId, updated.targetHealthCheckId, updated.name || "", undefined, undefined, false // dont send the alert because we just want to create the alert, not notify users yet ); } else if ( updated.hcHealth === "unknown" && existingHealthCheck.hcHealth !== "unknown" ) { // if the health is unknown, we want to fire an alert to notify users to enable health checks await fireHealthCheckUnknownAlert( updated.orgId, updated.targetHealthCheckId, updated.name, undefined, undefined, false // dont send the alert because we just want to create the alert, not notify users yet ); } else if ( updated.hcHealth === "healthy" && existingHealthCheck.hcHealth !== "healthy" ) { await fireHealthCheckHealthyAlert( updated.orgId, updated.targetHealthCheckId, updated.name, undefined, undefined, false // dont send the alert because we just want to create the alert, not notify users yet ); } // Push updated health check to newt if the site is a newt site const [newt] = await db .select() .from(newts) .where(eq(newts.siteId, updated.siteId)) .limit(1); if (newt) { await addStandaloneHealthCheck(newt.newtId, updated, newt.version); } return response(res, { data: { targetHealthCheckId: updated.targetHealthCheckId, siteId: updated.siteId ?? null, name: updated.name ?? null, hcEnabled: updated.hcEnabled, hcHealth: updated.hcHealth ?? null, hcMode: updated.hcMode ?? null, hcHostname: updated.hcHostname ?? null, hcPort: updated.hcPort ?? null, hcPath: updated.hcPath ?? null, hcScheme: updated.hcScheme ?? null, hcMethod: updated.hcMethod ?? null, hcInterval: updated.hcInterval ?? null, hcUnhealthyInterval: updated.hcUnhealthyInterval ?? null, hcTimeout: updated.hcTimeout ?? null, hcHeaders: updated.hcHeaders ?? null, hcFollowRedirects: updated.hcFollowRedirects ?? null, hcStatus: updated.hcStatus ?? null, hcTlsServerName: updated.hcTlsServerName ?? null, hcHealthyThreshold: updated.hcHealthyThreshold ?? null, hcUnhealthyThreshold: updated.hcUnhealthyThreshold ?? null }, success: true, error: false, message: "Standalone health check updated successfully", status: HttpCode.OK }); } catch (error) { logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } }