Create hcs freely

This commit is contained in:
Owen
2026-04-15 20:32:02 -07:00
parent a04e2a5e00
commit 1397e61643
18 changed files with 1881 additions and 72 deletions

View File

@@ -145,12 +145,15 @@ export enum ActionsEnum {
updateEventStreamingDestination = "updateEventStreamingDestination",
deleteEventStreamingDestination = "deleteEventStreamingDestination",
listEventStreamingDestinations = "listEventStreamingDestinations",
listHealthChecks = "listHealthChecks",
createAlertRule = "createAlertRule",
updateAlertRule = "updateAlertRule",
deleteAlertRule = "deleteAlertRule",
listAlertRules = "listAlertRules",
getAlertRule = "getAlertRule"
getAlertRule = "getAlertRule",
createHealthCheck = "createHealthCheck",
updateHealthCheck = "updateHealthCheck",
deleteHealthCheck = "deleteHealthCheck",
listHealthChecks = "listHealthChecks"
}
export async function checkUserActionPermission(

View File

@@ -140,6 +140,7 @@ export async function updateProxyResources(
const [newHealthcheck] = await trx
.insert(targetHealthCheck)
.values({
name: `${targetData.hostname}:${targetData.port}`,
targetId: newTarget.targetId,
hcEnabled: healthcheckData?.enabled || false,
hcPath: healthcheckData?.path,

View File

@@ -30,6 +30,7 @@ import * as user from "#private/routers/user";
import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks";
import {
verifyOrgAccess,
@@ -695,3 +696,38 @@ authenticated.get(
verifyUserHasAction(ActionsEnum.getAlertRule),
alertRule.getAlertRule
);
authenticated.get(
"/org/:orgId/health-checks",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listHealthChecks),
healthChecks.listHealthChecks
);
authenticated.put(
"/org/:orgId/health-check",
verifyValidLicense,
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createHealthCheck),
logActionAudit(ActionsEnum.createHealthCheck),
healthChecks.createHealthCheck
);
authenticated.post(
"/org/:orgId/health-check/:healthCheckId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateHealthCheck),
logActionAudit(ActionsEnum.updateHealthCheck),
healthChecks.updateHealthCheck
);
authenticated.delete(
"/org/:orgId/health-check/:healthCheckId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.deleteHealthCheck),
logActionAudit(ActionsEnum.deleteHealthCheck),
healthChecks.deleteHealthCheck
);

View File

@@ -0,0 +1,158 @@
/*
* 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 } 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";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const bodySchema = z.strictObject({
name: z.string().nonempty(),
hcEnabled: z.boolean().default(false),
hcMode: z.string().default("http"),
hcHostname: z.string().optional(),
hcPort: z.number().int().min(1).max(65535).optional(),
hcPath: z.string().optional(),
hcScheme: z.string().optional(),
hcMethod: z.string().default("GET"),
hcInterval: z.number().int().positive().default(30),
hcUnhealthyInterval: z.number().int().positive().default(30),
hcTimeout: z.number().int().positive().default(5),
hcHeaders: z.string().optional().nullable(),
hcFollowRedirects: z.boolean().default(true),
hcStatus: z.number().int().optional().nullable(),
hcTlsServerName: z.string().optional(),
hcHealthyThreshold: z.number().int().positive().default(1),
hcUnhealthyThreshold: z.number().int().positive().default(1)
});
export type CreateHealthCheckResponse = {
targetHealthCheckId: number;
};
registry.registerPath({
method: "put",
path: "/org/{orgId}/health-check",
description: "Create a health check for a specific organization.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema,
body: {
content: {
"application/json": {
schema: bodySchema
}
}
}
},
responses: {}
});
export async function createHealthCheck(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
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,
hcEnabled,
hcMode,
hcHostname,
hcPort,
hcPath,
hcScheme,
hcMethod,
hcInterval,
hcUnhealthyInterval,
hcTimeout,
hcHeaders,
hcFollowRedirects,
hcStatus,
hcTlsServerName,
hcHealthyThreshold,
hcUnhealthyThreshold
} = parsedBody.data;
const [record] = await db
.insert(targetHealthCheck)
.values({
targetId: null,
orgId,
name,
hcEnabled,
hcMode,
hcHostname: hcHostname ?? null,
hcPort: hcPort ?? null,
hcPath: hcPath ?? null,
hcScheme: hcScheme ?? null,
hcMethod,
hcInterval,
hcUnhealthyInterval,
hcTimeout,
hcHeaders: hcHeaders ?? null,
hcFollowRedirects,
hcStatus: hcStatus ?? null,
hcTlsServerName: hcTlsServerName ?? null,
hcHealthyThreshold,
hcUnhealthyThreshold
})
.returning();
return response<CreateHealthCheckResponse>(res, {
data: {
targetHealthCheckId: record.targetHealthCheckId
},
success: true,
error: false,
message: "Standalone health check created successfully",
status: HttpCode.CREATED
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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 } 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";
const paramsSchema = z
.object({
orgId: z.string().nonempty(),
healthCheckId: z
.string()
.transform(Number)
.pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "delete",
path: "/org/{orgId}/health-check/{healthCheckId}",
description: "Delete a health check for a specific organization.",
tags: [OpenAPITags.Org],
request: {
params: paramsSchema
},
responses: {}
});
export async function deleteHealthCheck(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
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 [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"
)
);
}
await db
.delete(targetHealthCheck)
.where(
and(
eq(targetHealthCheck.targetHealthCheckId, healthCheckId),
eq(targetHealthCheck.orgId, orgId),
isNull(targetHealthCheck.targetId)
)
);
return response<null>(res, {
data: null,
success: true,
error: false,
message: "Standalone health check deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -0,0 +1,17 @@
/*
* 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 "./listHealthChecks";
export * from "./createHealthCheck";
export * from "./updateHealthCheck";
export * from "./deleteHealthCheck";

View File

@@ -1,19 +1,33 @@
import { db, targetHealthCheck, targets, resources } from "@server/db";
/*
* 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, targetHealthCheck } 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 { OpenAPITags, registry } from "@server/openApi";
import { eq, sql, inArray } from "drizzle-orm";
import { and, eq, isNull, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
const listHealthChecksParamsSchema = z.strictObject({
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
});
const listHealthChecksSchema = z.object({
const querySchema = z.object({
limit: z
.string()
.optional()
@@ -28,29 +42,14 @@ const listHealthChecksSchema = z.object({
.pipe(z.int().nonnegative())
});
export type ListHealthChecksResponse = {
healthChecks: {
targetHealthCheckId: number;
resourceId: number;
resourceName: string;
hcEnabled: boolean;
hcHealth: "unknown" | "healthy" | "unhealthy";
}[];
pagination: {
total: number;
limit: number;
offset: number;
};
};
registry.registerPath({
method: "get",
path: "/org/{orgId}/health-checks",
description: "List health checks for all resources in an organization.",
tags: [OpenAPITags.Org, OpenAPITags.PublicResource],
description: "List health checks for an organization.",
tags: [OpenAPITags.Org],
request: {
params: listHealthChecksParamsSchema,
query: listHealthChecksSchema
params: paramsSchema,
query: querySchema
},
responses: {}
});
@@ -61,62 +60,71 @@ export async function listHealthChecks(
next: NextFunction
): Promise<any> {
try {
const parsedQuery = listHealthChecksSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const { limit, offset } = parsedQuery.data;
const parsedParams = listHealthChecksParamsSchema.safeParse(req.params);
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { limit, offset } = parsedQuery.data;
const whereClause = and(
eq(targetHealthCheck.orgId, orgId),
isNull(targetHealthCheck.targetId)
);
const list = await db
.select({
targetHealthCheckId: targetHealthCheck.targetHealthCheckId,
resourceId: resources.resourceId,
resourceName: resources.name,
hcEnabled: targetHealthCheck.hcEnabled,
hcHealth: targetHealthCheck.hcHealth
})
.select()
.from(targetHealthCheck)
.innerJoin(targets, eq(targets.targetId, targetHealthCheck.targetId))
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
.where(eq(resources.orgId, orgId))
.orderBy(sql`${resources.name} ASC`)
.where(whereClause)
.orderBy(sql`${targetHealthCheck.targetHealthCheckId} DESC`)
.limit(limit)
.offset(offset);
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(targetHealthCheck)
.innerJoin(targets, eq(targets.targetId, targetHealthCheck.targetId))
.innerJoin(resources, eq(resources.resourceId, targets.resourceId))
.where(eq(resources.orgId, orgId));
.where(whereClause);
return response<ListHealthChecksResponse>(res, {
data: {
healthChecks: list.map((row) => ({
targetHealthCheckId: row.targetHealthCheckId,
resourceId: row.resourceId,
resourceName: row.resourceName,
name: row.name ?? "",
hcEnabled: row.hcEnabled,
hcHealth: (row.hcHealth ?? "unknown") as
| "unknown"
| "healthy"
| "unhealthy"
| "unhealthy",
hcMode: row.hcMode ?? null,
hcHostname: row.hcHostname ?? null,
hcPort: row.hcPort ?? null,
hcPath: row.hcPath ?? null,
hcScheme: row.hcScheme ?? null,
hcMethod: row.hcMethod ?? null,
hcInterval: row.hcInterval ?? null,
hcUnhealthyInterval: row.hcUnhealthyInterval ?? null,
hcTimeout: row.hcTimeout ?? null,
hcHeaders: row.hcHeaders ?? null,
hcFollowRedirects: row.hcFollowRedirects ?? null,
hcStatus: row.hcStatus ?? null,
hcTlsServerName: row.hcTlsServerName ?? null,
hcHealthyThreshold: row.hcHealthyThreshold ?? null,
hcUnhealthyThreshold: row.hcUnhealthyThreshold ?? null
})),
pagination: {
total: count,
@@ -126,7 +134,7 @@ export async function listHealthChecks(
},
success: true,
error: false,
message: "Health checks retrieved successfully",
message: "Standalone health checks retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
@@ -135,4 +143,4 @@ export async function listHealthChecks(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
}

View File

@@ -0,0 +1,239 @@
/*
* 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 } 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";
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(),
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;
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<any> {
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,
hcEnabled,
hcMode,
hcHostname,
hcPort,
hcPath,
hcScheme,
hcMethod,
hcInterval,
hcUnhealthyInterval,
hcTimeout,
hcHeaders,
hcFollowRedirects,
hcStatus,
hcTlsServerName,
hcHealthyThreshold,
hcUnhealthyThreshold
} = parsedBody.data;
const updateData: Record<string, unknown> = {};
if (name !== undefined) updateData.name = name;
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 [updated] = await db
.update(targetHealthCheck)
.set(updateData)
.where(
and(
eq(
targetHealthCheck.targetHealthCheckId,
healthCheckId
),
eq(targetHealthCheck.orgId, orgId),
isNull(targetHealthCheck.targetId)
)
)
.returning();
return response<UpdateHealthCheckResponse>(res, {
data: {
targetHealthCheckId: updated.targetHealthCheckId,
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")
);
}
}

View File

@@ -427,13 +427,6 @@ authenticated.get(
resource.listResources
);
authenticated.get(
"/org/:orgId/health-checks",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listHealthChecks),
resource.listHealthChecks
);
authenticated.get(
"/org/:orgId/resource-names",
verifyOrgAccess,

View File

@@ -0,0 +1,28 @@
export type ListHealthChecksResponse = {
healthChecks: {
targetHealthCheckId: number;
name: string;
hcEnabled: boolean;
hcHealth: "unknown" | "healthy" | "unhealthy";
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;
}[];
pagination: {
total: number;
limit: number;
offset: number;
};
};

View File

@@ -32,4 +32,3 @@ export * from "./addUserToResource";
export * from "./removeUserFromResource";
export * from "./listAllResourceNames";
export * from "./removeEmailFromResourceWhitelist";
export * from "./listHealthChecks";

View File

@@ -228,6 +228,7 @@ export async function createTarget(
healthCheck = await db
.insert(targetHealthCheck)
.values({
name: `${targetData.ip}:${targetData.port}`,
targetId: newTarget[0].targetId,
hcEnabled: targetData.hcEnabled ?? false,
hcPath: targetData.hcPath ?? null,