From 015f910bdcbf1536b4c4fd342d30f7d2dd90c604 Mon Sep 17 00:00:00 2001
From: ajvpot <553597+ajvpot@users.noreply.github.com>
Date: Thu, 3 Jul 2025 00:00:00 +0000
Subject: [PATCH] no sqli pls
---
package-lock.json | 17 +++++++++++
package.json | 3 +-
src/components/MapView.tsx | 50 +++++++++++++++++++++++---------
src/lib/clickhouse/actions.ts | 33 +++++++++++++++------
src/lib/clickhouse/clickhouse.ts | 13 ++++-----
5 files changed, 85 insertions(+), 31 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5937d02..d73a7b6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "meshexplorer",
"version": "0.1.0",
"dependencies": {
+ "@clickhouse/client": "^1.11.2",
"@headlessui/react": "^2.2.4",
"@heroicons/react": "^2.2.0",
"@types/leaflet": "^1.9.19",
@@ -62,6 +63,22 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@clickhouse/client": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@clickhouse/client/-/client-1.11.2.tgz",
+ "integrity": "sha512-ZE7Q1qxsDNXCkGPf1zqmhpZpwAKxKT+1s4Z432J1Mb2Gm26Y4tG/sJoug81AfAJTt6s7taO2vzNBAKfSR3SStg==",
+ "dependencies": {
+ "@clickhouse/client-common": "1.11.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@clickhouse/client-common": {
+ "version": "1.11.2",
+ "resolved": "https://registry.npmjs.org/@clickhouse/client-common/-/client-common-1.11.2.tgz",
+ "integrity": "sha512-H4ECHqaipzMgiZKqpb1Z4N3Ofq+lVTCn8I59XsSynqrsfR4jWZD3PipXVvIzMpDmTMvrlJWrOwAdm0DMNiMQbA=="
+ },
"node_modules/@emnapi/core": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
diff --git a/package.json b/package.json
index 6b1935b..c220928 100644
--- a/package.json
+++ b/package.json
@@ -3,12 +3,13 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev --turbopack",
+ "dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
+ "@clickhouse/client": "^1.11.2",
"@headlessui/react": "^2.2.4",
"@heroicons/react": "^2.2.0",
"@types/leaflet": "^1.9.19",
diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx
index 13c522f..9fc9445 100644
--- a/src/components/MapView.tsx
+++ b/src/components/MapView.tsx
@@ -16,7 +16,7 @@ const DEFAULT = {
};
type NodePosition = {
- from_node_id: string;
+ node_id: string;
latitude: number;
longitude: number;
altitude?: number;
@@ -56,10 +56,14 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
});
const marker = L.marker([node.latitude, node.longitude], { icon });
(marker as any).options.nodeData = node;
- let popupHtml = `
ID: ${node.from_node_id}
Lat: ${node.latitude}
Lng: ${node.longitude}
`;
- if (node.altitude !== undefined) popupHtml += `
Alt: ${node.altitude}
`;
- if (node.last_seen) popupHtml += `
Last seen: ${node.last_seen}
`;
- if (node.type) popupHtml += `
Type: ${node.type}
`;
+ let popupHtml = `
`;
+ popupHtml += `
ID: ${node.node_id}
`;
+ popupHtml += `
Short Name: ${node.short_name ?? "-"}
`;
+ popupHtml += `
Type: ${node.type ?? "-"}
`;
+ popupHtml += `
Lat: ${node.latitude}
`;
+ popupHtml += `
Lng: ${node.longitude}
`;
+ popupHtml += `
Alt: ${node.altitude !== undefined ? node.altitude : "-"}
`;
+ popupHtml += `
Last seen: ${node.last_seen ?? "-"}
`;
popupHtml += `
`;
marker.bindPopup(popupHtml);
marker.addTo(map);
@@ -155,10 +159,14 @@ function ClusteredMarkers({ nodes }: ClusteredMarkersProps) {
});
const marker = L.marker([node.latitude, node.longitude], { icon });
(marker as any).options.nodeData = node;
- let popupHtml = `
ID: ${node.from_node_id}
Lat: ${node.latitude}
Lng: ${node.longitude}
`;
- if (node.altitude !== undefined) popupHtml += `
Alt: ${node.altitude}
`;
- if (node.last_seen) popupHtml += `
Last seen: ${node.last_seen}
`;
- if (node.type) popupHtml += `
Type: ${node.type}
`;
+ let popupHtml = `
`;
+ popupHtml += `
ID: ${node.node_id}
`;
+ popupHtml += `
Short Name: ${node.short_name ?? "-"}
`;
+ popupHtml += `
Type: ${node.type ?? "-"}
`;
+ popupHtml += `
Lat: ${node.latitude}
`;
+ popupHtml += `
Lng: ${node.longitude}
`;
+ popupHtml += `
Alt: ${node.altitude !== undefined ? node.altitude : "-"}
`;
+ popupHtml += `
Last seen: ${node.last_seen ?? "-"}
`;
popupHtml += `
`;
marker.bindPopup(popupHtml);
markers.addLayer(marker);
@@ -177,6 +185,7 @@ export default function MapView() {
const [nodePositions, setNodePositions] = useState
([]);
const [bounds, setBounds] = useState<[[number, number], [number, number]] | null>(null);
const [loading, setLoading] = useState(false);
+ const [lastResultCount, setLastResultCount] = useState(0);
const fetchController = useRef(null);
const lastRequestedBounds = useRef<[[number, number], [number, number]] | null>(null);
const { config } = useConfig ? useConfig() : { config: undefined };
@@ -231,7 +240,10 @@ export default function MapView() {
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then((data) => {
- if (Array.isArray(data)) setNodePositions(data);
+ if (Array.isArray(data)) {
+ setNodePositions(data);
+ setLastResultCount(data.length);
+ }
if (fetchController.current === controller) setLoading(false);
})
.catch((err) => {
@@ -255,7 +267,7 @@ export default function MapView() {
useMapEvents({
moveend: (e) => {
const b = e.target.getBounds();
- const buffer = 0.2; // 20% buffer
+ const buffer = 0.05; // 5% buffer
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
const newBounds: [[number, number], [number, number]] = [
@@ -268,13 +280,18 @@ export default function MapView() {
b.getNorthEast().lng + lngDiff * buffer,
],
];
- if (!lastRequestedBounds.current || !isBoundsInside(newBounds, lastRequestedBounds.current)) {
+ // Only always refetch if we have too many nodes depending on clustering setting.
+ if (
+ (lastResultCount > (config?.clustering ? 5000: 1000)) ||
+ !lastRequestedBounds.current ||
+ !isBoundsInside(newBounds, lastRequestedBounds.current)
+ ) {
setBounds(newBounds);
}
},
zoomend: (e) => {
const b = e.target.getBounds();
- const buffer = 0.2; // 20% buffer
+ const buffer = 0.05; // 5% buffer
const latDiff = b.getNorthEast().lat - b.getSouthWest().lat;
const lngDiff = b.getNorthEast().lng - b.getSouthWest().lng;
const newBounds: [[number, number], [number, number]] = [
@@ -287,7 +304,12 @@ export default function MapView() {
b.getNorthEast().lng + lngDiff * buffer,
],
];
- if (!lastRequestedBounds.current || !isBoundsInside(newBounds, lastRequestedBounds.current)) {
+ // Only always refetch if clustering is disabled and lastResultCount > 1000
+ if (
+ (config?.clustering === false && lastResultCount > 1000) ||
+ !lastRequestedBounds.current ||
+ !isBoundsInside(newBounds, lastRequestedBounds.current)
+ ) {
setBounds(newBounds);
}
},
diff --git a/src/lib/clickhouse/actions.ts b/src/lib/clickhouse/actions.ts
index 18b8668..c35f527 100644
--- a/src/lib/clickhouse/actions.ts
+++ b/src/lib/clickhouse/actions.ts
@@ -6,19 +6,34 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
"latitude IS NOT NULL",
"longitude IS NOT NULL"
];
- if (minLat) where.push(`latitude >= ${minLat}`);
- if (maxLat) where.push(`latitude <= ${maxLat}`);
- if (minLng) where.push(`longitude >= ${minLng}`);
- if (maxLng) where.push(`longitude <= ${maxLng}`);
+ const params: Record = {};
+ if (minLat !== null && minLat !== undefined && minLat !== "") {
+ where.push(`latitude >= {minLat:Float64}`);
+ params.minLat = Number(minLat);
+ }
+ if (maxLat !== null && maxLat !== undefined && maxLat !== "") {
+ where.push(`latitude <= {maxLat:Float64}`);
+ params.maxLat = Number(maxLat);
+ }
+ if (minLng !== null && minLng !== undefined && minLng !== "") {
+ where.push(`longitude >= {minLng:Float64}`);
+ params.minLng = Number(minLng);
+ }
+ if (maxLng !== null && maxLng !== undefined && maxLng !== "") {
+ where.push(`longitude <= {maxLng:Float64}`);
+ params.maxLng = Number(maxLng);
+ }
if (nodeTypes && nodeTypes.length > 0) {
- const types = nodeTypes.map(t => `'${t}'`).join(",");
- where.push(`type IN (${types})`);
+ where.push(`type IN {nodeTypes:Array(String)}`);
+ params.nodeTypes = nodeTypes;
}
if (lastSeen !== null && lastSeen !== undefined && lastSeen !== "") {
- where.push(`last_seen >= now() - INTERVAL ${lastSeen} SECOND`);
+ where.push(`last_seen >= now() - INTERVAL {lastSeen:UInt32} SECOND`);
+ params.lastSeen = Number(lastSeen);
}
const query = `SELECT node_id, name, short_name, latitude, longitude, last_seen, type FROM unified_latest_nodeinfo WHERE ${where.join(" AND ")}`;
- const rows = await clickhouse.query(query).toPromise();
+ const resultSet = await clickhouse.query({ query, query_params: params, format: 'JSONEachRow' });
+ const rows = await resultSet.json();
return rows as Array<{
node_id: string;
name?: string | null;
@@ -28,4 +43,4 @@ export async function getNodePositions({ minLat, maxLat, minLng, maxLng, nodeTyp
last_seen: string;
type: string;
}>;
- }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/lib/clickhouse/clickhouse.ts b/src/lib/clickhouse/clickhouse.ts
index c06b736..9ccc6df 100644
--- a/src/lib/clickhouse/clickhouse.ts
+++ b/src/lib/clickhouse/clickhouse.ts
@@ -1,14 +1,13 @@
-import { ClickHouse } from 'clickhouse';
+import { createClient } from '@clickhouse/client';
const host = process.env.CLICKHOUSE_HOST || 'localhost';
const port = process.env.CLICKHOUSE_PORT || '8123';
const user = process.env.CLICKHOUSE_USER || 'default';
const password = process.env.CLICKHOUSE_PASSWORD || 'password';
-export const clickhouse = new ClickHouse({
- url: `http://${host}`,
- port: Number(port),
- basicAuth: { username: user, password },
- isUseGzip: false,
- format: 'json',
+export const clickhouse = createClient({
+ host: `http://${host}:${port}`,
+ username: user,
+ password: password,
+ // You can add more options as needed, e.g. database, compression, etc.
});
\ No newline at end of file