Show targets and status icons in the dashboard

This commit is contained in:
Pallavi
2025-08-24 20:57:27 +05:30
committed by Pallavi Kumari
parent da0196a308
commit 1b3eb32bf4
9 changed files with 847 additions and 123 deletions

View File

@@ -23,6 +23,8 @@ export enum ActionsEnum {
deleteResource = "deleteResource",
getResource = "getResource",
listResources = "listResources",
tcpCheck = "tcpCheck",
batchTcpCheck = "batchTcpCheck",
updateResource = "updateResource",
createTarget = "createTarget",
deleteTarget = "deleteTarget",

View File

@@ -306,6 +306,20 @@ authenticated.get(
resource.listResources
);
authenticated.post(
"/org/:orgId/resources/tcp-check",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.tcpCheck),
resource.tcpCheck
);
authenticated.post(
"/org/:orgId/resources/tcp-check-batch",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.batchTcpCheck),
resource.batchTcpCheck
);
authenticated.get(
"/org/:orgId/user-resources",
verifyOrgAccess,

View File

@@ -25,3 +25,4 @@ export * from "./getUserResources";
export * from "./setResourceHeaderAuth";
export * from "./addEmailToResourceWhitelist";
export * from "./removeEmailFromResourceWhitelist";
export * from "./tcpCheck";

View File

@@ -6,7 +6,8 @@ import {
userResources,
roleResources,
resourcePassword,
resourcePincode
resourcePincode,
targets,
} from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -40,6 +41,53 @@ const listResourcesSchema = z.object({
.pipe(z.number().int().nonnegative())
});
// (resource fields + a single joined target)
type JoinedRow = {
resourceId: number;
niceId: string;
name: string;
ssl: boolean;
fullDomain: string | null;
passwordId: number | null;
sso: boolean;
pincodeId: number | null;
whitelist: boolean;
http: boolean;
protocol: string;
proxyPort: number | null;
enabled: boolean;
domainId: string | null;
targetId: number | null;
targetIp: string | null;
targetPort: number | null;
targetEnabled: boolean | null;
};
// grouped by resource with targets[])
export type ResourceWithTargets = {
resourceId: number;
name: string;
ssl: boolean;
fullDomain: string | null;
passwordId: number | null;
sso: boolean;
pincodeId: number | null;
whitelist: boolean;
http: boolean;
protocol: string;
proxyPort: number | null;
enabled: boolean;
domainId: string | null;
niceId: string | null;
targets: Array<{
targetId: number;
ip: string;
port: number;
enabled: boolean;
}>;
};
function queryResources(accessibleResourceIds: number[], orgId: string) {
return db
.select({
@@ -57,7 +105,13 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
enabled: resources.enabled,
domainId: resources.domainId,
niceId: resources.niceId,
headerAuthId: resourceHeaderAuth.headerAuthId
headerAuthId: resourceHeaderAuth.headerAuthId,
targetId: targets.targetId,
targetIp: targets.ip,
targetPort: targets.port,
targetEnabled: targets.enabled,
})
.from(resources)
.leftJoin(
@@ -72,6 +126,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId)
)
.leftJoin(targets, eq(targets.resourceId, resources.resourceId))
.where(
and(
inArray(resources.resourceId, accessibleResourceIds),
@@ -81,7 +136,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
}
export type ListResourcesResponse = {
resources: NonNullable<Awaited<ReturnType<typeof queryResources>>>;
resources: ResourceWithTargets[];
pagination: { total: number; limit: number; offset: number };
};
@@ -146,7 +201,7 @@ export async function listResources(
);
}
let accessibleResources;
let accessibleResources: Array<{ resourceId: number }>;
if (req.user) {
accessibleResources = await db
.select({
@@ -183,9 +238,49 @@ export async function listResources(
const baseQuery = queryResources(accessibleResourceIds, orgId);
const resourcesList = await baseQuery!.limit(limit).offset(offset);
const rows: JoinedRow[] = await baseQuery.limit(limit).offset(offset);
// avoids TS issues with reduce/never[]
const map = new Map<number, ResourceWithTargets>();
for (const row of rows) {
let entry = map.get(row.resourceId);
if (!entry) {
entry = {
resourceId: row.resourceId,
niceId: row.niceId,
name: row.name,
ssl: row.ssl,
fullDomain: row.fullDomain,
passwordId: row.passwordId,
sso: row.sso,
pincodeId: row.pincodeId,
whitelist: row.whitelist,
http: row.http,
protocol: row.protocol,
proxyPort: row.proxyPort,
enabled: row.enabled,
domainId: row.domainId,
targets: [],
};
map.set(row.resourceId, entry);
}
// Push target if present (left join can be null)
if (row.targetId != null && row.targetIp && row.targetPort != null && row.targetEnabled != null) {
entry.targets.push({
targetId: row.targetId,
ip: row.targetIp,
port: row.targetPort,
enabled: row.targetEnabled,
});
}
}
const resourcesList: ResourceWithTargets[] = Array.from(map.values());
const totalCountResult = await countQuery;
const totalCount = totalCountResult[0].count;
const totalCount = totalCountResult[0]?.count ?? 0;
return response<ListResourcesResponse>(res, {
data: {

View File

@@ -0,0 +1,290 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import * as net from "net";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
const tcpCheckSchema = z
.object({
host: z.string().min(1, "Host is required"),
port: z.number().int().min(1).max(65535),
timeout: z.number().int().min(1000).max(30000).optional().default(5000)
})
.strict();
export type TcpCheckResponse = {
connected: boolean;
host: string;
port: number;
responseTime?: number;
error?: string;
};
registry.registerPath({
method: "post",
path: "/org/{orgId}/resources/tcp-check",
description: "Check TCP connectivity to a host and port",
tags: [OpenAPITags.Resource],
request: {
body: {
content: {
"application/json": {
schema: tcpCheckSchema
}
}
}
},
responses: {
200: {
description: "TCP check result",
content: {
"application/json": {
schema: z.object({
success: z.boolean(),
data: z.object({
connected: z.boolean(),
host: z.string(),
port: z.number(),
responseTime: z.number().optional(),
error: z.string().optional()
}),
message: z.string()
})
}
}
}
}
});
function checkTcpConnection(host: string, port: number, timeout: number): Promise<TcpCheckResponse> {
return new Promise((resolve) => {
const startTime = Date.now();
const socket = new net.Socket();
const cleanup = () => {
socket.removeAllListeners();
if (!socket.destroyed) {
socket.destroy();
}
};
const timer = setTimeout(() => {
cleanup();
resolve({
connected: false,
host,
port,
error: 'Connection timeout'
});
}, timeout);
socket.setTimeout(timeout);
socket.on('connect', () => {
const responseTime = Date.now() - startTime;
clearTimeout(timer);
cleanup();
resolve({
connected: true,
host,
port,
responseTime
});
});
socket.on('error', (error) => {
clearTimeout(timer);
cleanup();
resolve({
connected: false,
host,
port,
error: error.message
});
});
socket.on('timeout', () => {
clearTimeout(timer);
cleanup();
resolve({
connected: false,
host,
port,
error: 'Socket timeout'
});
});
try {
socket.connect(port, host);
} catch (error) {
clearTimeout(timer);
cleanup();
resolve({
connected: false,
host,
port,
error: error instanceof Error ? error.message : 'Unknown connection error'
});
}
});
}
export async function tcpCheck(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = tcpCheckSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { host, port, timeout } = parsedBody.data;
const result = await checkTcpConnection(host, port, timeout);
logger.info(`TCP check for ${host}:${port} - Connected: ${result.connected}`, {
host,
port,
connected: result.connected,
responseTime: result.responseTime,
error: result.error
});
return response<TcpCheckResponse>(res, {
data: result,
success: true,
error: false,
message: `TCP check completed for ${host}:${port}`,
status: HttpCode.OK
});
} catch (error) {
logger.error("TCP check error:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred during TCP check"
)
);
}
}
// Batch TCP check endpoint for checking multiple resources at once
const batchTcpCheckSchema = z
.object({
checks: z.array(z.object({
id: z.number().int().positive(),
host: z.string().min(1),
port: z.number().int().min(1).max(65535)
})).max(50), // Limit to 50 concurrent checks
timeout: z.number().int().min(1000).max(30000).optional().default(5000)
})
.strict();
export type BatchTcpCheckResponse = {
results: Array<TcpCheckResponse & { id: number }>;
};
registry.registerPath({
method: "post",
path: "/org/{orgId}/resources/tcp-check-batch",
description: "Check TCP connectivity to multiple hosts and ports",
tags: [OpenAPITags.Resource],
request: {
body: {
content: {
"application/json": {
schema: batchTcpCheckSchema
}
}
}
},
responses: {
200: {
description: "Batch TCP check results",
content: {
"application/json": {
schema: z.object({
success: z.boolean(),
data: z.object({
results: z.array(z.object({
id: z.number(),
connected: z.boolean(),
host: z.string(),
port: z.number(),
responseTime: z.number().optional(),
error: z.string().optional()
}))
}),
message: z.string()
})
}
}
}
}
});
export async function batchTcpCheck(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = batchTcpCheckSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { checks, timeout } = parsedBody.data;
// all TCP checks concurrently
const checkPromises = checks.map(async (check) => {
const result = await checkTcpConnection(check.host, check.port, timeout);
return {
id: check.id,
...result
};
});
const results = await Promise.all(checkPromises);
logger.info(`Batch TCP check completed for ${checks.length} resources`, {
totalChecks: checks.length,
successfulConnections: results.filter(r => r.connected).length,
failedConnections: results.filter(r => !r.connected).length
});
return response<BatchTcpCheckResponse>(res, {
data: { results },
success: true,
error: false,
message: `Batch TCP check completed for ${checks.length} resources`,
status: HttpCode.OK
});
} catch (error) {
logger.error("Batch TCP check error:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An error occurred during batch TCP check"
)
);
}
}